This commit is contained in:
Isaiah Williams 2018-09-25 10:26:08 -05:00 коммит произвёл GitHub
Родитель 1208724b2f
Коммит ca70f5fa3f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
361 изменённых файлов: 66917 добавлений и 59 удалений

77
.editorconfig Normal file
Просмотреть файл

@ -0,0 +1,77 @@
# To learn more about .editorconfig see https://aka.ms/editorconfigdocs
root = true
# Don't use tabs for indentation.
[*]
indent_style = space
end_of_line = crlf
# (Please don't specify an indent_size here; that has too many unintended consequences.)
# Code files
[*.{cs,csx,vb,vbx}]
indent_size = 4
# Xml project files
[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
indent_size = 2
# Xml config files
[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
indent_size = 2
# JSON files
[*.json]
indent_size = 2
# Dotnet code style settings:
[*.{cs, vb}]
# Sort using and Import directives with System.* appearing first
dotnet_sort_system_directives_first = true
# Avoid "this." and "Me." if not necessary
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_event = false:suggestion
# Use language keywords instead of framework type names for type references
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
# Suggest more modern language features when available
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
# CSharp code style settings:
[*.cs]
# Prefer "var" everywhere
csharp_style_var_for_built_in_types = false:suggestion
csharp_style_var_when_type_is_apparent = false:suggestion
csharp_style_var_elsewhere = false:suggestion
# Prefer method-like constructs to have a block body
csharp_style_expression_bodied_methods = false:none
csharp_style_expression_bodied_constructors = false:none
csharp_style_expression_bodied_operators = false:none
# Prefer property-like constructs to have an expression-body
csharp_style_expression_bodied_properties = true:none
csharp_style_expression_bodied_indexers = true:none
csharp_style_expression_bodied_accessors = true:none
# Suggest more modern language features when available
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
csharp_style_throw_expression = true:suggestion
csharp_style_conditional_delegate_call = true:suggestion
# Newline settings
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true

63
.gitignore поставляемый
Просмотреть файл

@ -24,14 +24,11 @@ bld/
[Oo]bj/
[Ll]og/
# Visual Studio 2015/2017 cache/options directory
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
@ -45,29 +42,20 @@ TestResult.xml
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
**/Properties/launchSettings.json
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
@ -105,9 +93,6 @@ ipch/
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
@ -128,10 +113,6 @@ _TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
@ -167,7 +148,7 @@ publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
@ -180,11 +161,11 @@ PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
**/packages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
#!**/packages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
@ -202,7 +183,6 @@ AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
# Visual Studio cache files
# files ending in .cache can be ignored
@ -221,10 +201,6 @@ ClientBin/
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
@ -239,8 +215,6 @@ _UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
@ -251,7 +225,6 @@ ServiceFabricBackup/
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
# Microsoft Fakes
FakesAssemblies/
@ -263,6 +236,9 @@ FakesAssemblies/
.ntvs_analysis.dat
node_modules/
# Typescript v1 declaration files
typings/
# Visual Studio 6 build log
*.plg
@ -302,9 +278,6 @@ __pycache__/
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
@ -314,17 +287,9 @@ __pycache__/
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

3
CODE_OF_CONDUCT.md Normal file
Просмотреть файл

@ -0,0 +1,3 @@
# Code of Conduct
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.

80
CONTRIBUTING.md Normal file
Просмотреть файл

@ -0,0 +1,80 @@
# Contributing to Partner Smart Office
This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to and actually do, grant us
the rights to use your contribution. View the [Contributor License Agreement](https://cla.microsoft.com) for more details.
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
- [Code of Conduct](#code-of-conduct)
- [Issues and Bugs](#finding-issues)
- [Feature Requests](#requesting-features)
- [Submission Guidelines](#submission-guidelines)
## Code of Conduct
Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
## Finding Issues
If you find a bug in the source code or a mistake in the documentation, you can help us by
[submitting an issue](#submitting-an-issue) to the GitHub Repository. Even better, you can
[submit a Pull Request](#submitting-a-pull-request) with a fix.
## Requesting Features
You can *request* a new feature by [submitting an issue](#submitting-an-issue) to the GitHub
Repository. If you would like to *implement* a new feature, please submit an issue with
a proposal for your work first, to be sure that we can use it.
**Small Features** can be crafted and directly [submitted as a Pull Request](#submitting-a-pull-request).
## Submission Guidelines
### Submitting an Issue
Before you submit an issue, search the archive, maybe your question was already answered.
If your issue appears to be a bug and hasn't been reported, open a new issue.
Help us to maximize the effort we can spend fixing issues and add new
features, by not reporting duplicate issues. Providing the following information will increase the
chances of your issue being dealt with quickly:
- **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps
- **Version** - what version is affected (e.g. 0.1.2)
- **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you
- **Browsers and Operating System** - is this a problem with all browsers?
- **Reproduce the Error** - provide a live example or an unambiguous set of steps
- **Related Issues** - has a similar issue been reported before?
- **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be
causing the problem (line of code or commit)
You can file new issues by providing the above information at the [corresponding repository's issues link](https://github.com/Microsoft/Partner-Center-Storefront/issues/new).
### Submitting a Pull Request
Before you submit your Pull Request (PR) consider the following guidelines:
- [Search the repository](https://github.com/Microsoft/Partner-Center-Storefront/pulls) for an open or closed PR
that relates to your submission. You don't want to duplicate effort.
- Make your changes in a new git fork:
- Commit your changes using a descriptive commit message
- Push your fork to GitHub:
- In GitHub, create a pull request
- If we suggest changes then:
- Make the required updates.
- Rebase your fork and force push to your GitHub repository (this will update your Pull Request):
```shell
git rebase master -i
git push -f
```
That is it! Thank you for your contribution!

Просмотреть файл

@ -0,0 +1,38 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.28010.2036
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{345764CB-D256-42E4-9D32-1101588F542D}"
ProjectSection(SolutionItems) = preProject
CODE_OF_CONDUCT.md = CODE_OF_CONDUCT.md
CONTRIBUTING.md = CONTRIBUTING.md
LICENSE = LICENSE
README.md = README.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3CD8EE7C-5C97-49ED-9EAA-90AF670FD41A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Storefront", "src\Storefront\Storefront.csproj", "{07C22DE5-B22A-474D-B593-9F5DA84C6DD8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{07C22DE5-B22A-474D-B593-9F5DA84C6DD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{07C22DE5-B22A-474D-B593-9F5DA84C6DD8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{07C22DE5-B22A-474D-B593-9F5DA84C6DD8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{07C22DE5-B22A-474D-B593-9F5DA84C6DD8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{07C22DE5-B22A-474D-B593-9F5DA84C6DD8} = {3CD8EE7C-5C97-49ED-9EAA-90AF670FD41A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {51E97A25-EEAA-4F88-A98B-9AE3DCE2057B}
EndGlobalSection
EndGlobal

104
README.md
Просмотреть файл

@ -1,14 +1,98 @@
# Partner Web Storefront
# Contributing
![Build status](https://dev.azure.com/partnercenter/storefront/_apis/build/status/storefront-github-master-CI)
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.microsoft.com.
## Overview
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.
<p>
A web application that acts as a store front for Microsoft partners and enables them to sell Microsoft offers to their customers.
The application gives partners the following features:
<ol>
<li>Configure the Microsoft offers they would like to sell to their customers. Partners can set the price and append extra details.</li>
<li>Configure the portal branding to reflect their company branding. This includes setting the company name, header icons, etc...</li>
<li>Payment. Partners can configure their PayPal pro account which will receive payments from customers.</li>
</ol>
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
The store front application currently supports the following languages (French, Spanish, German and Japanese) along with English which serves as the fallback language.
The store front uses the partner's default locale to configure the Locale (Currencies, Date formats, Localized offers in the repository) using the Partner Profile from partner center.
Customers can <ol>
<li>Use the portal to view the offers available, purchase the quantities they need and make a payment from the storefront.</li>
<li>Log back in and view their subscriptions, purchase extra seats or renew about to expire subscriptions.</li>
<li>View all the subscriptions (whether they have purchased via the Store front or have been managed for them from Partner Center) in the My Account page after they login. </li>
</ol>
</p>
## Deployment
The portal can be deployed from within Partner Center at: <a href="https://partnercenter.microsoft.com/en-us/pcv/webstore/preparedeployment">https://partnercenter.microsoft.com/en-us/pcv/webstore/preparedeployment</a>.
There is also a deployment project included in the solution through which, deployment can be started with the specified inputs.
[![Deploy to Azure](http://azuredeploy.net/deploybutton.png)](https://azuredeploy.net/)
[![Visualize](http://armviz.io/visualizebutton.png)](http://armviz.io/#/?load=https%3A%2F%2Fraw.githubusercontent.com%2FPartnerCenterSamples%2FReseller-Web-Application%2Fmaster%2Fazuredeploy.json)
## Build & Deploy on your own
If you are interested to fork and custom build/deploy the store front. We recommend reading [this blog post](https://blogs.msdn.microsoft.com/iwilliams/2016/12/17/reseller-storefront/) by [Isaiah Williams](https://github.com/isaiahwilliams)
Clone the source code and perform the following steps:
<ol>
<li>
Go to Partner Center, Account Settings, App Management and onboard a new Web App. Copy the application ID, application secret
and the partner tenant ID into the following settings in Web.Config:
```xml
<!-- Enter your partner center onboarded AAD application ID here -->
<add key="partnerCenter.applicationId" value="" />
<!-- Enter your partner center onboarded AAD application secret here -->
<add key="partnerCenter.applicationSecret" value="" />
<!-- Enter your partner center AAD tenant ID here -->
<add key="partnerCenter.AadTenantId" value="" />
```
</li>
<li>
Create a Web application in your Azure AD tenant. The portal will assume the identity of this application. Change the
following settings in Web.Config to your AD application information:
```xml
<!-- The AAD client ID of the application running the web portal -->
<add key="webPortal.clientId" value="" />
<!-- The AAD client secret of the application running the web portal -->
<add key="webPortal.clientSecret" value="" />
<!-- The AAD tenant ID of the application running the web portal -->
<add key="webPortal.AadTenantId" value="" />
<!-- The AAD client ID of the application running the web portal -->
<add key="webPortal.clientId" value="" />
<!-- The AAD client secret of the application running the web portal -->
<add key="webPortal.clientSecret" value="" />
<!-- The AAD tenant ID of the application running the web portal -->
<add key="webPortal.AadTenantId" value="" />
```
</li>
<li>
Provision an Azure storage account which will store the portal's assets and information. Copy its connection string to:
```xml
<!-- The Azure storage connection string which will host the web portal's settings and customers repository. -->
<add key="webPortal.azureStorageConnectionString" value="" />
```
</div>
</li>
<li>
Optionally, specify a REDIS cache connection string to improve performance.
```xml
<!-- The Azure Redis cache connection string. Empty value will disable caching. -->
<add key="webPortal.cacheConnectionString" value="" />
```
</li>
</ol>

Просмотреть файл

@ -0,0 +1,38 @@
// -----------------------------------------------------------------------
// <copyright file="FilterConfig.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront
{
using System.Web.Http.Filters;
using System.Web.Mvc;
using Filters.Mvc;
/// <summary>
/// Configures application filters.
/// </summary>
public class FilterConfig
{
/// <summary>
/// Registers global MVC filters.
/// </summary>
/// <param name="filters">The global MVC filter collection.</param>
public static void RegisterGlobalMvcFilters(GlobalFilterCollection filters)
{
filters.Add(new AuthenticationFilterAttribute());
filters.Add(new AiHandleErrorAttribute());
}
/// <summary>
/// Registers global Web API filters.
/// </summary>
/// <param name="filters">The global web API filter collection.</param>
public static void RegisterWebApiFilters(HttpFilterCollection filters)
{
filters.Add(new Filters.WebApi.AuthenticationFilterAttribute());
filters.Add(new Filters.WebApi.ErrorHandlerAttribute());
}
}
}

Просмотреть файл

@ -0,0 +1,31 @@
// -----------------------------------------------------------------------
// <copyright file="RouteConfig.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront
{
using System.Web.Mvc;
using System.Web.Routing;
/// <summary>
/// Holds routing configuration.
/// </summary>
public static class RouteConfig
{
/// <summary>
/// Registers routes.
/// </summary>
/// <param name="routes">A collection of routes.</param>
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional });
}
}
}

Просмотреть файл

@ -0,0 +1,124 @@
// -----------------------------------------------------------------------
// <copyright file="Startup.Auth.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Net;
using System.Threading.Tasks;
using System.Web;
using BusinessLogic;
using Configuration;
using Exceptions;
using global::Owin;
using IdentityModel.Tokens;
using Models;
using Owin.Security;
using Owin.Security.Cookies;
using Owin.Security.OpenIdConnect;
/// <summary>
/// Application start up class.
/// </summary>
public partial class Startup
{
/// <summary>
/// The Azure global admin user role.
/// </summary>
private const string GlobalAdminUserRole = "Company Administrator";
/// <summary>
/// Configures application authentication.
/// </summary>
/// <param name="app">The application to configure.</param>
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions { });
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = ApplicationConfiguration.ActiveDirectoryClientID,
Authority = ApplicationConfiguration.ActiveDirectoryEndPoint + "common",
TokenValidationParameters = new TokenValidationParameters()
{
// instead of using the default validation (validating against a single issuer value, as we do in line of business apps),
// we inject our own multitenant validation logic
ValidateIssuer = false,
},
Notifications = new OpenIdConnectAuthenticationNotifications()
{
RedirectToIdentityProvider = (context) =>
{
context.ProtocolMessage.Parameters.Add("lc", Resources.Culture.LCID.ToString(CultureInfo.InvariantCulture));
return Task.FromResult(0);
},
AuthorizationCodeReceived = async (context) =>
{
string userTenantId = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
string signedInUserObjectId = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
IGraphClient graphClient = new GraphClient(
userTenantId,
context.Code,
new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)));
List<RoleModel> roles = await graphClient.GetDirectoryRolesAsync(signedInUserObjectId).ConfigureAwait(false);
foreach (RoleModel role in roles)
{
context.AuthenticationTicket.Identity.AddClaim(new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Role, role.DisplayName));
}
if (userTenantId != ApplicationConfiguration.ActiveDirectoryTenantId)
{
string partnerCenterCustomerId = string.Empty;
// Check to see if this login came from the tenant of a customer of the partner
PartnerCenter.Models.Customers.Customer customerDetails = await ApplicationDomain.Instance.PartnerCenterClient.Customers.ById(userTenantId).GetAsync().ConfigureAwait(false);
// indeed a customer
partnerCenterCustomerId = customerDetails.Id;
if (!string.IsNullOrWhiteSpace(partnerCenterCustomerId))
{
// add the customer ID to the claims
context.AuthenticationTicket.Identity.AddClaim(new System.Security.Claims.Claim("PartnerCenterCustomerID", partnerCenterCustomerId));
// fire off call to retrieve this customer's subscriptions and populate the CustomerSubscriptions Repository.
}
}
else
{
if (context.AuthenticationTicket.Identity.FindFirst(System.Security.Claims.ClaimTypes.Role).Value != Startup.GlobalAdminUserRole)
{
// this login came from the partner's tenant, only allow admins to access the site, non admins will only
// see the unauthenticated experience but they can't configure the portal nor can purchase
Trace.TraceInformation("Blocked log in from non admin partner user: {0}", signedInUserObjectId);
throw new UnauthorizedException(Resources.NonAdminUnauthorizedMessage, HttpStatusCode.Unauthorized);
}
}
},
AuthenticationFailed = (context) =>
{
// redirect to the error page
string errorMessage = (context.Exception.InnerException == null) ?
context.Exception.Message : context.Exception.InnerException.Message;
context.OwinContext.Response.Redirect($"/Home/Error?errorMessage={errorMessage}");
context.HandleResponse();
return Task.FromResult(0);
}
}
});
}
}
}

Просмотреть файл

@ -0,0 +1,34 @@
// -----------------------------------------------------------------------
// <copyright file="WebApiConfig.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront
{
using System.Web.Http;
using System.Web.Mvc;
using Models.Validators;
/// <summary>
/// Configures Web API routes.
/// </summary>
public static class WebApiConfig
{
/// <summary>
/// Registers web API routes.
/// </summary>
/// <param name="configuration">HTTP configuration.</param>
public static void Register(HttpConfiguration configuration)
{
configuration.MapHttpAttributeRoutes();
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(ExpiryDateInTenYearsAttribute), typeof(RangeAttributeAdapter));
configuration.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional });
}
}
}

Просмотреть файл

@ -0,0 +1,140 @@
<?xml version="1.0" encoding="utf-8"?>
<ApplicationInsights xmlns="http://schemas.microsoft.com/ApplicationInsights/2013/Settings">
<!--
Learn more about Application Insights configuration with ApplicationInsights.config here:
http://go.microsoft.com/fwlink/?LinkID=513840
Note: If not present, please add <InstrumentationKey>Your Key</InstrumentationKey> to the top of this file.
-->
<TelemetryInitializers>
<Add Type="Microsoft.ApplicationInsights.DependencyCollector.HttpDependenciesParsingTelemetryInitializer, Microsoft.AI.DependencyCollector"/>
<Add Type="Microsoft.ApplicationInsights.WindowsServer.AzureRoleEnvironmentTelemetryInitializer, Microsoft.AI.WindowsServer"/>
<Add Type="Microsoft.ApplicationInsights.WindowsServer.AzureWebAppRoleEnvironmentTelemetryInitializer, Microsoft.AI.WindowsServer"/>
<Add Type="Microsoft.ApplicationInsights.WindowsServer.BuildInfoConfigComponentVersionTelemetryInitializer, Microsoft.AI.WindowsServer"/>
<Add Type="Microsoft.ApplicationInsights.Web.WebTestTelemetryInitializer, Microsoft.AI.Web"/>
<Add Type="Microsoft.ApplicationInsights.Web.SyntheticUserAgentTelemetryInitializer, Microsoft.AI.Web">
<!-- Extended list of bots:
search|spider|crawl|Bot|Monitor|BrowserMob|BingPreview|PagePeeker|WebThumb|URL2PNG|ZooShot|GomezA|Google SketchUp|Read Later|KTXN|KHTE|Keynote|Pingdom|AlwaysOn|zao|borg|oegp|silk|Xenu|zeal|NING|htdig|lycos|slurp|teoma|voila|yahoo|Sogou|CiBra|Nutch|Java|JNLP|Daumoa|Genieo|ichiro|larbin|pompos|Scrapy|snappy|speedy|vortex|favicon|indexer|Riddler|scooter|scraper|scrubby|WhatWeb|WinHTTP|voyager|archiver|Icarus6j|mogimogi|Netvibes|altavista|charlotte|findlinks|Retreiver|TLSProber|WordPress|wsr-agent|http client|Python-urllib|AppEngine-Google|semanticdiscovery|facebookexternalhit|web/snippet|Google-HTTP-Java-Client-->
<Filters>search|spider|crawl|Bot|Monitor|AlwaysOn</Filters>
</Add>
<Add Type="Microsoft.ApplicationInsights.Web.ClientIpHeaderTelemetryInitializer, Microsoft.AI.Web"/>
<Add Type="Microsoft.ApplicationInsights.Web.OperationNameTelemetryInitializer, Microsoft.AI.Web"/>
<Add Type="Microsoft.ApplicationInsights.Web.OperationCorrelationTelemetryInitializer, Microsoft.AI.Web"/>
<Add Type="Microsoft.ApplicationInsights.Web.UserTelemetryInitializer, Microsoft.AI.Web"/>
<Add Type="Microsoft.ApplicationInsights.Web.AuthenticatedUserIdTelemetryInitializer, Microsoft.AI.Web"/>
<Add Type="Microsoft.ApplicationInsights.Web.AccountIdTelemetryInitializer, Microsoft.AI.Web"/>
<Add Type="Microsoft.ApplicationInsights.Web.SessionTelemetryInitializer, Microsoft.AI.Web"/>
</TelemetryInitializers>
<TelemetryModules>
<Add Type="Microsoft.ApplicationInsights.DependencyCollector.DependencyTrackingTelemetryModule, Microsoft.AI.DependencyCollector">
<ExcludeComponentCorrelationHttpHeadersOnDomains>
<!--
Requests to the following hostnames will not be modified by adding correlation headers.
Add entries here to exclude additional hostnames.
NOTE: this configuration will be lost upon NuGet upgrade.
-->
<Add>core.windows.net</Add>
<Add>core.chinacloudapi.cn</Add>
<Add>core.cloudapi.de</Add>
<Add>core.usgovcloudapi.net</Add>
<Add>localhost</Add>
<Add>127.0.0.1</Add>
</ExcludeComponentCorrelationHttpHeadersOnDomains>
<IncludeDiagnosticSourceActivities>
<Add>Microsoft.Azure.EventHubs</Add>
<Add>Microsoft.Azure.ServiceBus</Add>
</IncludeDiagnosticSourceActivities>
</Add>
<Add Type="Microsoft.ApplicationInsights.Extensibility.PerfCounterCollector.PerformanceCollectorModule, Microsoft.AI.PerfCounterCollector">
<!--
Use the following syntax here to collect additional performance counters:
<Counters>
<Add PerformanceCounter="\Process(??APP_WIN32_PROC??)\Handle Count" ReportAs="Process handle count" />
...
</Counters>
PerformanceCounter must be either \CategoryName(InstanceName)\CounterName or \CategoryName\CounterName
NOTE: performance counters configuration will be lost upon NuGet upgrade.
The following placeholders are supported as InstanceName:
??APP_WIN32_PROC?? - instance name of the application process for Win32 counters.
??APP_W3SVC_PROC?? - instance name of the application IIS worker process for IIS/ASP.NET counters.
??APP_CLR_PROC?? - instance name of the application CLR process for .NET counters.
-->
</Add>
<Add Type="Microsoft.ApplicationInsights.Extensibility.PerfCounterCollector.QuickPulse.QuickPulseTelemetryModule, Microsoft.AI.PerfCounterCollector"/>
<Add Type="Microsoft.ApplicationInsights.WindowsServer.AppServicesHeartbeatTelemetryModule, Microsoft.AI.WindowsServer"/>
<Add Type="Microsoft.ApplicationInsights.WindowsServer.AzureInstanceMetadataTelemetryModule, Microsoft.AI.WindowsServer">
<!--
Remove individual fields collected here by adding them to the ApplicationInsighs.HeartbeatProvider
with the following syntax:
<Add Type="Microsoft.ApplicationInsights.Extensibility.Implementation.Tracing.DiagnosticsTelemetryModule, Microsoft.ApplicationInsights">
<ExcludedHeartbeatProperties>
<Add>osType</Add>
<Add>location</Add>
<Add>name</Add>
<Add>offer</Add>
<Add>platformFaultDomain</Add>
<Add>platformUpdateDomain</Add>
<Add>publisher</Add>
<Add>sku</Add>
<Add>version</Add>
<Add>vmId</Add>
<Add>vmSize</Add>
<Add>subscriptionId</Add>
<Add>resourceGroupName</Add>
</ExcludedHeartbeatProperties>
</Add>
NOTE: exclusions will be lost upon upgrade.
-->
</Add>
<Add Type="Microsoft.ApplicationInsights.WindowsServer.DeveloperModeWithDebuggerAttachedTelemetryModule, Microsoft.AI.WindowsServer"/>
<Add Type="Microsoft.ApplicationInsights.WindowsServer.UnhandledExceptionTelemetryModule, Microsoft.AI.WindowsServer"/>
<Add Type="Microsoft.ApplicationInsights.WindowsServer.UnobservedExceptionTelemetryModule, Microsoft.AI.WindowsServer">
<!--</Add>
<Add Type="Microsoft.ApplicationInsights.WindowsServer.FirstChanceExceptionStatisticsTelemetryModule, Microsoft.AI.WindowsServer">-->
</Add>
<Add Type="Microsoft.ApplicationInsights.Web.RequestTrackingTelemetryModule, Microsoft.AI.Web">
<Handlers>
<!--
Add entries here to filter out additional handlers:
NOTE: handler configuration will be lost upon NuGet upgrade.
-->
<Add>Microsoft.VisualStudio.Web.PageInspector.Runtime.Tracing.RequestDataHttpHandler</Add>
<Add>System.Web.StaticFileHandler</Add>
<Add>System.Web.Handlers.AssemblyResourceLoader</Add>
<Add>System.Web.Optimization.BundleHandler</Add>
<Add>System.Web.Script.Services.ScriptHandlerFactory</Add>
<Add>System.Web.Handlers.TraceHandler</Add>
<Add>System.Web.Services.Discovery.DiscoveryRequestHandler</Add>
<Add>System.Web.HttpDebugHandler</Add>
</Handlers>
</Add>
<Add Type="Microsoft.ApplicationInsights.Web.ExceptionTrackingTelemetryModule, Microsoft.AI.Web"/>
<Add Type="Microsoft.ApplicationInsights.Web.AspNetDiagnosticTelemetryModule, Microsoft.AI.Web"/>
</TelemetryModules>
<ApplicationIdProvider Type="Microsoft.ApplicationInsights.Extensibility.Implementation.ApplicationId.ApplicationInsightsApplicationIdProvider, Microsoft.ApplicationInsights"/>
<TelemetryProcessors>
<Add Type="Microsoft.ApplicationInsights.Extensibility.PerfCounterCollector.QuickPulse.QuickPulseTelemetryProcessor, Microsoft.AI.PerfCounterCollector"/>
<Add Type="Microsoft.ApplicationInsights.Extensibility.AutocollectedMetricsExtractor, Microsoft.ApplicationInsights"/>
<Add Type="Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel.AdaptiveSamplingTelemetryProcessor, Microsoft.AI.ServerTelemetryChannel">
<MaxTelemetryItemsPerSecond>5</MaxTelemetryItemsPerSecond>
<ExcludedTypes>Event</ExcludedTypes>
</Add>
<Add Type="Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel.AdaptiveSamplingTelemetryProcessor, Microsoft.AI.ServerTelemetryChannel">
<MaxTelemetryItemsPerSecond>5</MaxTelemetryItemsPerSecond>
<IncludedTypes>Event</IncludedTypes>
</Add>
</TelemetryProcessors>
<TelemetryChannel Type="Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel.ServerTelemetryChannel, Microsoft.AI.ServerTelemetryChannel"/>
<!--
Learn more about Application Insights configuration with ApplicationInsights.config here:
http://go.microsoft.com/fwlink/?LinkID=513840
Note: If not present, please add <InstrumentationKey>Your Key</InstrumentationKey> to the top of this file.
--></ApplicationInsights>

Просмотреть файл

@ -0,0 +1,162 @@
// -----------------------------------------------------------------------
// <copyright file="ApplicationDomain.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic
{
using System.Configuration;
using System.Threading.Tasks;
using Commerce;
using Configuration;
using Offers;
using PartnerCenter.Extensions;
/// <summary>
/// The application domain holds key domain objects needed across the application.
/// </summary>
public class ApplicationDomain
{
/// <summary>
/// Prevents a default instance of the <see cref="ApplicationDomain"/> class from being created.
/// </summary>
private ApplicationDomain()
{
}
/// <summary>
/// Gets an instance of the application domain.
/// </summary>
public static ApplicationDomain Instance { get; private set; }
/// <summary>
/// Gets a Partner Center SDK client.
/// </summary>
public IAggregatePartner PartnerCenterClient { get; private set; }
/// <summary>
/// Gets the partner offers repository.
/// </summary>
public PartnerOffersRepository OffersRepository { get; private set; }
/// <summary>
/// Gets the Azure storage service.
/// </summary>
public AzureStorageService AzureStorageService { get; private set; }
/// <summary>
/// Gets the caching service.
/// </summary>
public CachingService CachingService { get; private set; }
/// <summary>
/// Gets the Microsoft offer logo indexer.
/// </summary>
public MicrosoftOfferLogoIndexer MicrosoftOfferLogoIndexer { get; private set; }
/// <summary>
/// Gets the portal branding service.
/// </summary>
public PortalBranding PortalBranding { get; private set; }
/// <summary>
/// Gets the portal localization service.
/// </summary>
public PortalLocalization PortalLocalization { get; private set; }
/// <summary>
/// Gets the portal payment configuration repository.
/// </summary>
public PaymentConfigurationRepository PaymentConfigurationRepository { get; private set; }
/// <summary>
/// Gets the portal PreApprovedCustomers configuration repository.
/// </summary>
public PreApprovedCustomersRepository PreApprovedCustomersRepository { get; private set; }
/// <summary>
/// Gets the customer subscriptions repository.
/// </summary>
public CustomerSubscriptionsRepository CustomerSubscriptionsRepository { get; private set; }
/// <summary>
/// Gets the customer purchases repository.
/// </summary>
public CustomerPurchasesRepository CustomerPurchasesRepository { get; private set; }
/// <summary>
/// Gets the customer orders repository.
/// </summary>
public OrdersRepository CustomerOrdersRepository { get; private set; }
/// <summary>
/// Gets the portal telemetry service.
/// </summary>
public TelemetryService TelemetryService { get; private set; }
/// <summary>
/// Gets the customer registration repository.
/// </summary>
public CustomerRegistrationRepository CustomerRegistrationRepository { get; private set; }
/// <summary>
/// Initializes the core application domain objects.
/// </summary>
/// <returns>A task.</returns>
public static async Task BootstrapAsync()
{
if (Instance == null)
{
Instance = new ApplicationDomain();
Instance.PartnerCenterClient = await AcquirePartnerCenterAccessAsync();
Instance.PortalLocalization = new PortalLocalization(Instance);
await Instance.PortalLocalization.InitializeAsync();
}
}
/// <summary>
/// Initializes the application domain objects.
/// </summary>
/// <returns>A task.</returns>
public static async Task InitializeAsync()
{
if (Instance != null)
{
Instance.AzureStorageService = new AzureStorageService(ApplicationConfiguration.AzureStorageConnectionString, ApplicationConfiguration.AzureStorageConnectionEndpointSuffix);
Instance.CachingService = new CachingService(Instance, ApplicationConfiguration.CacheConnectionString);
Instance.OffersRepository = new PartnerOffersRepository(Instance);
Instance.MicrosoftOfferLogoIndexer = new MicrosoftOfferLogoIndexer(Instance);
Instance.PortalBranding = new PortalBranding(Instance);
Instance.PaymentConfigurationRepository = new PaymentConfigurationRepository(Instance);
Instance.PreApprovedCustomersRepository = new PreApprovedCustomersRepository(Instance);
Instance.CustomerSubscriptionsRepository = new CustomerSubscriptionsRepository(Instance);
Instance.CustomerPurchasesRepository = new CustomerPurchasesRepository(ApplicationDomain.Instance);
Instance.CustomerOrdersRepository = new OrdersRepository(ApplicationDomain.Instance);
Instance.TelemetryService = new TelemetryService(Instance);
Instance.CustomerRegistrationRepository = new CustomerRegistrationRepository(ApplicationDomain.Instance);
await Instance.TelemetryService.InitializeAsync();
}
}
/// <summary>
/// Authenticates with the Partner Center APIs.
/// </summary>
/// <returns>A Partner Center API client.</returns>
private static async Task<IAggregatePartner> AcquirePartnerCenterAccessAsync()
{
PartnerService.Instance.ApiRootUrl = ConfigurationManager.AppSettings["partnerCenter.apiEndPoint"];
PartnerService.Instance.ApplicationName = "Web Store Front V1.4";
var credentials = await PartnerCredentials.Instance.GenerateByApplicationCredentialsAsync(
ConfigurationManager.AppSettings["partnercenter.applicationId"],
ConfigurationManager.AppSettings["partnercenter.applicationSecret"],
ConfigurationManager.AppSettings["partnercenter.AadTenantId"],
ConfigurationManager.AppSettings["aadEndpoint"],
ConfigurationManager.AppSettings["aadGraphEndpoint"]);
return PartnerService.Instance.CreatePartnerOperations(credentials);
}
}
}

Просмотреть файл

@ -0,0 +1,288 @@
// -----------------------------------------------------------------------
// <copyright file="AzureStorageService.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic
{
using System;
using System.Configuration;
using System.Globalization;
using System.Threading.Tasks;
using WindowsAzure.Storage;
using WindowsAzure.Storage.Blob;
using WindowsAzure.Storage.Table;
/// <summary>
/// Provides Azure storage assets.
/// </summary>
public class AzureStorageService
{
/// <summary>
/// The name of the portal assets blob container.
/// </summary>
private const string PrivatePortalAssetsBlobContainerName = "customerportalassets";
/// <summary>
/// The name of the portal asserts blob container which contains publicly available blobs.
/// This is useful for storing images which the browser can access.
/// </summary>
private const string PublicPortalAssetsBlobContainerName = "publiccustomerportalassets";
/// <summary>
/// The name of the portal customers blob container.
/// </summary>
private const string PrivatePortalCustomerBlobContainerName = "customerportalregistration";
/// <summary>
/// The name of the Partner Center customers Azure table.
/// </summary>
private const string CustomersTableName = "PartnerCenterCustomers";
/// <summary>
/// The name of the customer subscriptions Azure table.
/// </summary>
private const string CustomerSubscriptionsTableName = "CustomerSubscriptions";
/// <summary>
/// The name of the customer purchases Azure table.
/// </summary>
private const string CustomerPurchasesTableName = "CustomerPurchases";
/// <summary>
/// The name of the customer orders Azure table.
/// </summary>
private const string CustomerOrdersTableName = "PreApprovedCustomerOrders";
/// <summary>
/// The name of the customer Azure table.
/// </summary>
private const string CustomerRegistrationTableName = "CustomerRegistrations";
/// <summary>
/// The Azure cloud storage account.
/// </summary>
private CloudStorageAccount storageAccount;
/// <summary>
/// The BLOB container which contains the portal's configuration assets.
/// </summary>
private CloudBlobContainer privateBlobContainer;
/// <summary>
/// The BLOB container which contains the public portal's assets.
/// </summary>
private CloudBlobContainer publicBlobContainer;
/// <summary>
/// The Azure partner center customers table.
/// </summary>
private CloudTable partnerCenterCustomersTable;
/// <summary>
/// The Azure customer subscriptions table.
/// </summary>
private CloudTable customerSubscriptionsTable;
/// <summary>
/// The Azure customer purchases table.
/// </summary>
private CloudTable customerPurchasesTable;
/// <summary>
/// The Azure customer orders table.
/// </summary>
private CloudTable customerOrdersTable;
/// <summary>
/// The Azure customer registration table.
/// </summary>
private CloudTable customerRegistrationTable;
/// <summary>
/// Initializes a new instance of the <see cref="AzureStorageService"/> class.
/// </summary>
/// <param name="azureStorageConnectionString">The Azure storage connection string required to access the customer portal assets.</param>
/// <param name="azureStorageConnectionEndpointSuffix">The Azure storage connection endpoint suffix.</param>
public AzureStorageService(string azureStorageConnectionString, string azureStorageConnectionEndpointSuffix)
{
azureStorageConnectionString.AssertNotEmpty(nameof(azureStorageConnectionString));
azureStorageConnectionEndpointSuffix.AssertNotEmpty(nameof(azureStorageConnectionEndpointSuffix));
if (CloudStorageAccount.TryParse(azureStorageConnectionString, out CloudStorageAccount cloudStorageAccount))
{
if (azureStorageConnectionString.Equals("UseDevelopmentStorage=true", StringComparison.InvariantCultureIgnoreCase))
{
storageAccount = new CloudStorageAccount(
cloudStorageAccount.Credentials,
cloudStorageAccount.BlobStorageUri,
cloudStorageAccount.QueueStorageUri,
cloudStorageAccount.TableStorageUri,
cloudStorageAccount.FileStorageUri);
}
else
{
storageAccount = new CloudStorageAccount(cloudStorageAccount.Credentials, endpointSuffix: azureStorageConnectionEndpointSuffix, useHttps: true);
}
}
else
{
throw new ConfigurationErrorsException("webPortal.azureStorageConnectionString setting not valid in web.config");
}
}
/// <summary>
/// Generates a new BLOB reference to store a new asset.
/// </summary>
/// <param name="blobContainer">The Blob container in which to create the BLOB.</param>
/// <param name="blobPrefix">The BLOB name prefix to use.</param>
/// <returns>The new BLOB reference.</returns>
public async Task<CloudBlockBlob> GenerateNewBlobReferenceAsync(CloudBlobContainer blobContainer, string blobPrefix)
{
blobContainer.AssertNotNull(nameof(blobContainer));
blobPrefix = blobPrefix ?? "asset";
const string BlobNameFormat = "{0}{1}";
CloudBlockBlob newBlob = null;
do
{
newBlob = blobContainer.GetBlockBlobReference(string.Format(
CultureInfo.InvariantCulture,
BlobNameFormat,
blobPrefix,
new Random().Next().ToString(CultureInfo.InvariantCulture)));
}
while (await newBlob.ExistsAsync().ConfigureAwait(false));
return newBlob;
}
/// <summary>
/// Returns a cloud BLOB container reference which can be used to manage the customer portal assets.
/// </summary>
/// <returns>The customer portal assets BLOB container.</returns>
public async Task<CloudBlobContainer> GetPrivateCustomerPortalAssetsBlobContainerAsync()
{
if (privateBlobContainer == null)
{
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
privateBlobContainer = blobClient.GetContainerReference(PrivatePortalAssetsBlobContainerName);
}
await privateBlobContainer.CreateIfNotExistsAsync().ConfigureAwait(false);
return privateBlobContainer;
}
/// <summary>
/// Returns a cloud BLOB container reference which can be used to manage the public customer portal assets.
/// </summary>
/// <returns>The public customer portal assets BLOB container.</returns>
public async Task<CloudBlobContainer> GetPublicCustomerPortalAssetsBlobContainerAsync()
{
if (publicBlobContainer == null)
{
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
publicBlobContainer = blobClient.GetContainerReference(PublicPortalAssetsBlobContainerName);
}
if (!await this.publicBlobContainer.ExistsAsync().ConfigureAwait(false))
{
await this.publicBlobContainer.CreateAsync().ConfigureAwait(false);
BlobContainerPermissions permissions = await this.publicBlobContainer.GetPermissionsAsync().ConfigureAwait(false);
permissions.PublicAccess = BlobContainerPublicAccessType.Blob;
await publicBlobContainer.SetPermissionsAsync(permissions).ConfigureAwait(false);
}
return publicBlobContainer;
}
/// <summary>
/// Gets the Partner Center customers table.
/// </summary>
/// <returns>The Partner Center customers table.</returns>
public async Task<CloudTable> GetPartnerCenterCustomersTableAsync()
{
if (this.partnerCenterCustomersTable == null)
{
CloudTableClient tableClient = this.storageAccount.CreateCloudTableClient();
this.partnerCenterCustomersTable = tableClient.GetTableReference(CustomersTableName);
}
// someone can delete the table externally
await partnerCenterCustomersTable.CreateIfNotExistsAsync().ConfigureAwait(false);
return partnerCenterCustomersTable;
}
/// <summary>
/// Gets the customer subscriptions table.
/// </summary>
/// <returns>The customer subscriptions table.</returns>
public async Task<CloudTable> GetCustomerSubscriptionsTableAsync()
{
if (customerSubscriptionsTable == null)
{
CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
customerSubscriptionsTable = tableClient.GetTableReference(CustomerSubscriptionsTableName);
}
// someone can delete the table externally
await customerSubscriptionsTable.CreateIfNotExistsAsync().ConfigureAwait(false);
return customerSubscriptionsTable;
}
/// <summary>
/// Gets the customer purchases table.
/// </summary>
/// <returns>The customer purchases table.</returns>
public async Task<CloudTable> GetCustomerPurchasesTableAsync()
{
if (customerPurchasesTable == null)
{
CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
customerPurchasesTable = tableClient.GetTableReference(CustomerPurchasesTableName);
}
// someone can delete the table externally
await customerPurchasesTable.CreateIfNotExistsAsync().ConfigureAwait(false);
return customerPurchasesTable;
}
/// <summary>
/// Gets the customer orders table.
/// </summary>
/// <returns>The customer purchases table.</returns>
public async Task<CloudTable> GetCustomerOrdersTableAsync()
{
if (customerOrdersTable == null)
{
CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
customerOrdersTable = tableClient.GetTableReference(CustomerOrdersTableName);
}
// someone can delete the table externally
await customerOrdersTable.CreateIfNotExistsAsync().ConfigureAwait(false);
return customerOrdersTable;
}
/// <summary>
/// Gets the customer registration table.
/// </summary>
/// <returns>The customer registration table.</returns>
public async Task<CloudTable> GetCustomerRegistrationTableAsync()
{
if (customerRegistrationTable == null)
{
CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
customerRegistrationTable = tableClient.GetTableReference(CustomerRegistrationTableName);
}
// someone can delete the table externally
await customerRegistrationTable.CreateIfNotExistsAsync().ConfigureAwait(false);
return customerRegistrationTable;
}
}
}

Просмотреть файл

@ -0,0 +1,239 @@
// -----------------------------------------------------------------------
// <copyright file="PortalBranding.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic
{
using System;
using System.IO;
using System.Net.Mail;
using System.Threading.Tasks;
using Exceptions;
using Models;
using Newtonsoft.Json;
using WindowsAzure.Storage.Blob;
/// <summary>
/// Implements business behavior for the portal branding configuration. Enables clients to read and update the branding.
/// </summary>
public class PortalBranding : DomainObject
{
/// <summary>
/// The portal branding key in the cache.
/// </summary>
private const string PortalBrandingCacheKey = "PortalBranding";
/// <summary>
/// The Azure BLOB name for the portal branding.
/// </summary>
private const string PortalBrandingBlobName = "portalbranding";
/// <summary>
/// Initializes a new instance of the <see cref="PortalBranding"/> class.
/// </summary>
/// <param name="applicationDomain">An application domain instance.</param>
public PortalBranding(ApplicationDomain applicationDomain) : base(applicationDomain)
{
}
/// <summary>
/// Checks if the portal branding had been configured or not.
/// </summary>
/// <returns>True if the branding was configured and stored, false otherwise.</returns>
public async Task<bool> IsConfiguredAsync()
{
var portalBrandingBlob = await this.GetPortalBrandingBlobAsync();
return await portalBrandingBlob.ExistsAsync();
}
/// <summary>
/// Retrieves the portal branding.
/// </summary>
/// <returns>The portal branding information.</returns>
public async Task<BrandingConfiguration> RetrieveAsync()
{
var portalBranding = await this.ApplicationDomain.CachingService
.FetchAsync<BrandingConfiguration>(PortalBranding.PortalBrandingCacheKey);
if (portalBranding == null)
{
var portalBrandingBlob = await this.GetPortalBrandingBlobAsync();
portalBranding = new BrandingConfiguration();
if (await portalBrandingBlob.ExistsAsync())
{
portalBranding = JsonConvert.DeserializeObject<BrandingConfiguration>(await portalBrandingBlob.DownloadTextAsync());
await this.NormalizeAsync(portalBranding);
}
else
{
// portal branding has not been configured yet
portalBranding.OrganizationName = Resources.DefaultOrganizationName;
}
// cache the portal branding
await this.ApplicationDomain.CachingService.StoreAsync<BrandingConfiguration>(
PortalBranding.PortalBrandingCacheKey,
portalBranding);
}
return portalBranding;
}
/// <summary>
/// Updates the portal branding.
/// </summary>
/// <param name="updatedBrandingConfiguration">The new portal branding.</param>
/// <returns>The updated portal branding.</returns>
public async Task<BrandingConfiguration> UpdateAsync(BrandingConfiguration updatedBrandingConfiguration)
{
updatedBrandingConfiguration.AssertNotNull(nameof(updatedBrandingConfiguration));
await this.NormalizeAsync(updatedBrandingConfiguration);
var portalBrandingBlob = await this.GetPortalBrandingBlobAsync();
await portalBrandingBlob.UploadTextAsync(JsonConvert.SerializeObject(updatedBrandingConfiguration));
// invalidate the cache, we do not update it to avoid race condition between web instances
await this.ApplicationDomain.CachingService.ClearAsync(PortalBrandingCacheKey);
// re-initialize the telemetry service because the configuration might have changed.
await this.ApplicationDomain.TelemetryService.InitializeAsync();
return updatedBrandingConfiguration;
}
/// <summary>
/// Applies business rules to <see cref="BrandingConfiguration"/> instances.
/// </summary>
/// <param name="brandingConfiguration">A branding configuration instance.</param>
/// <returns>A task.</returns>
private async Task NormalizeAsync(BrandingConfiguration brandingConfiguration)
{
brandingConfiguration.AssertNotNull(nameof(brandingConfiguration));
brandingConfiguration.OrganizationName.AssertNotEmpty("OrganizationName");
if (brandingConfiguration.ContactUs == null)
{
throw new PartnerDomainException(ErrorCode.InvalidInput, Resources.ContactUsSectionNotFound).AddDetail("Field", "ContactUs");
}
brandingConfiguration.ContactUs.Email.AssertNotEmpty("Email");
try
{
new MailAddress(brandingConfiguration.ContactUs.Email);
}
catch (FormatException)
{
throw new PartnerDomainException(ErrorCode.InvalidInput, Resources.InvalidContactUsEmailAddress).AddDetail("Field", "ContactUs.Email");
}
try
{
brandingConfiguration.ContactUs.Phone.AssertNotEmpty("ContactUs.Phone");
}
catch (ArgumentException)
{
throw new PartnerDomainException(ErrorCode.InvalidInput, Resources.InvalidContactUsPhoneExceptionMessage).AddDetail("Field", "ContactUs.Phone");
}
if (brandingConfiguration.ContactSales == null)
{
// default the contact sales to the contact us information
brandingConfiguration.ContactSales = new ContactUsInformation()
{
Email = brandingConfiguration.ContactUs.Email,
Phone = brandingConfiguration.ContactUs.Phone
};
}
else
{
if (string.IsNullOrWhiteSpace(brandingConfiguration.ContactSales.Email))
{
brandingConfiguration.ContactSales.Email = brandingConfiguration.ContactUs.Email;
}
else
{
try
{
new MailAddress(brandingConfiguration.ContactSales.Email);
}
catch (FormatException)
{
throw new PartnerDomainException(ErrorCode.InvalidInput, Resources.InvalidContactSalesEmailAddress).AddDetail("Field", "ContactSales.Email");
}
}
if (string.IsNullOrWhiteSpace(brandingConfiguration.ContactSales.Phone))
{
brandingConfiguration.ContactSales.Phone = brandingConfiguration.ContactUs.Phone;
}
else
{
try
{
brandingConfiguration.ContactSales.Phone.AssertNotEmpty("ContactSales.Phone");
}
catch (ArgumentException)
{
throw new PartnerDomainException(ErrorCode.InvalidInput, Resources.InvalidContactSalesPhoneExceptionMessage).AddDetail("Field", "ContactSales.Phone");
}
}
}
if (brandingConfiguration.OrganizationLogoContent != null)
{
// there is an logo image specified, upload it to BLOB storage and setup the URI property to point to it
brandingConfiguration.OrganizationLogo = await this.UploadStreamToBlobStorageAsync(
"OrganizationLogo",
brandingConfiguration.OrganizationLogoContent);
brandingConfiguration.OrganizationLogoContent = null;
}
if (brandingConfiguration.HeaderImageContent != null)
{
// there is a header image specified, upload it to BLOB storage and setup the URI property to point to it
brandingConfiguration.HeaderImage = await this.UploadStreamToBlobStorageAsync(
"HeaderImage",
brandingConfiguration.HeaderImageContent);
brandingConfiguration.HeaderImageContent = null;
}
}
/// <summary>
/// Uploads a stream to a new asset BLOB.
/// </summary>
/// <param name="blobNamePrefix">The BLOB name prefix to use.</param>
/// <param name="streamToUpload">The stream to be uploaded.</param>
/// <returns>The uploaded BLOB's URI.</returns>
private async Task<Uri> UploadStreamToBlobStorageAsync(string blobNamePrefix, Stream streamToUpload)
{
streamToUpload.AssertNotNull(nameof(streamToUpload));
var blob = await this.ApplicationDomain.AzureStorageService.GenerateNewBlobReferenceAsync(
await this.ApplicationDomain.AzureStorageService.GetPublicCustomerPortalAssetsBlobContainerAsync(),
blobNamePrefix);
await blob.UploadFromStreamAsync(streamToUpload);
return blob.Uri;
}
/// <summary>
/// Retrieves the portal branding BLOB reference.
/// </summary>
/// <returns>The portal branding BLOB.</returns>
private async Task<CloudBlockBlob> GetPortalBrandingBlobAsync()
{
var portalAssetsBlobContainer = await this.ApplicationDomain.AzureStorageService.GetPrivateCustomerPortalAssetsBlobContainerAsync();
return portalAssetsBlobContainer.GetBlockBlobReference(PortalBranding.PortalBrandingBlobName);
}
}
}

Просмотреть файл

@ -0,0 +1,128 @@
// -----------------------------------------------------------------------
// <copyright file="CachingService.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic
{
using System;
using System.Threading.Tasks;
using Newtonsoft.Json;
using StackExchange.Redis;
/// <summary>
/// Facilitates caching frequently used objects to enhance the portal's performance.
/// </summary>
public class CachingService : DomainObject
{
/// <summary>
/// The cache connection string.
/// </summary>
private readonly string cacheConnectionString;
/// <summary>
/// Indicates whether caching is enabled or not.
/// </summary>
private readonly bool isCashingEnabled;
/// <summary>
/// The cache connection.
/// </summary>
private IConnectionMultiplexer cacheConnection;
/// <summary>
/// Initializes a new instance of the <see cref="CachingService"/> class.
/// </summary>
/// <param name="applicationDomain">An application domain instance.</param>
/// <param name="cacheConnectionString">The cache connection string.</param>
public CachingService(ApplicationDomain applicationDomain, string cacheConnectionString) : base(applicationDomain)
{
this.cacheConnectionString = cacheConnectionString;
this.isCashingEnabled = !string.IsNullOrWhiteSpace(this.cacheConnectionString);
}
/// <summary>
/// Stores an object in the cache.
/// </summary>
/// <typeparam name="TEntity">The object type.</typeparam>
/// <param name="key">The object's key in the cache.</param>
/// <param name="objectToCache">The object to cache.</param>
/// <param name="expiresAfter">An optional expiry time.</param>
/// <returns>A task.</returns>
public async Task StoreAsync<TEntity>(string key, TEntity objectToCache, TimeSpan? expiresAfter = null)
{
key.AssertNotEmpty(nameof(key));
objectToCache.AssertNotNull(nameof(objectToCache));
if (this.isCashingEnabled)
{
IDatabase cache = await this.GetCacheReferenceAsync();
await cache.StringSetAsync(key, JsonConvert.SerializeObject(objectToCache), expiresAfter);
}
}
/// <summary>
/// Retrieves an object from the cache.
/// </summary>
/// <typeparam name="TEntity">The object type.</typeparam>
/// <param name="key">The object's key in the cache.</param>
/// <returns>The object if found, null if not found.</returns>
public async Task<TEntity> FetchAsync<TEntity>(string key) where TEntity : class
{
key.AssertNotEmpty(nameof(key));
if (this.isCashingEnabled)
{
IDatabase cache = await this.GetCacheReferenceAsync();
RedisValue objectValue = await cache.StringGetAsync(key);
if (objectValue.IsNullOrEmpty)
{
return null;
}
return JsonConvert.DeserializeObject<TEntity>(objectValue);
}
else
{
return null;
}
}
/// <summary>
/// Removes an object from the cache.
/// </summary>
/// <param name="key">The object's key in the cache.</param>
/// <returns>A task.</returns>
public async Task ClearAsync(string key)
{
key.AssertNotEmpty(nameof(key));
if (this.isCashingEnabled)
{
IDatabase cache = await this.GetCacheReferenceAsync();
await cache.KeyDeleteAsync(key);
}
}
/// <summary>
/// Establishes connection with the cache.
/// </summary>
/// <returns>The cache reference.</returns>
private async Task<IDatabase> GetCacheReferenceAsync()
{
if (!this.isCashingEnabled)
{
throw new InvalidOperationException("Caching is disabled");
}
if (this.cacheConnection == null)
{
this.cacheConnection = await ConnectionMultiplexer.ConnectAsync(this.cacheConnectionString);
}
return this.cacheConnection.GetDatabase();
}
}
}

Просмотреть файл

@ -0,0 +1,331 @@
// -----------------------------------------------------------------------
// <copyright file="CommerceOperations.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Infrastructure;
using Models;
using PartnerCenter.Models.Orders;
using Transactions;
/// <summary>
/// Implements the portal commerce transactions.
/// </summary>
public class CommerceOperations : DomainObject, ICommerceOperations
{
/// <summary>
/// Initializes a new instance of the <see cref="CommerceOperations"/> class.
/// </summary>
/// <param name="applicationDomain">An application domain instance.</param>
/// <param name="customerId">The customer ID who owns the transaction.</param>
/// <param name="paymentGateway">A payment gateway to use for processing payments resulting from the transaction.</param>
public CommerceOperations(ApplicationDomain applicationDomain, string customerId, IPaymentGateway paymentGateway) : base(applicationDomain)
{
customerId.AssertNotEmpty(nameof(customerId));
paymentGateway.AssertNotNull(nameof(paymentGateway));
this.CustomerId = customerId;
this.PaymentGateway = paymentGateway;
}
/// <summary>
/// Gets the customer ID who owns the transaction.
/// </summary>
public string CustomerId { get; private set; }
/// <summary>
/// Gets the payment gateway used to process payments.
/// </summary>
public IPaymentGateway PaymentGateway { get; private set; }
/// <summary>
/// Calculates the amount to charge for buying an extra additional seat for the remainder of a subscription's lease.
/// </summary>
/// <param name="expiryDate">The subscription's expiry date.</param>
/// <param name="yearlyRatePerSeat">The subscription's yearly price per seat.</param>
/// <returns>The prorated amount to charge for the new extra seat.</returns>
public static decimal CalculateProratedSeatCharge(DateTime expiryDate, decimal yearlyRatePerSeat)
{
DateTime rightNow = DateTime.UtcNow;
expiryDate = expiryDate.ToUniversalTime();
decimal dailyChargePerSeat = yearlyRatePerSeat / 365m;
// 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);
return remainingDaysTillExpiry * dailyChargePerSeat;
}
/// <summary>
/// Purchases one or more partner offers.
/// </summary>
/// <param name="order">The order to execute.</param>
/// <returns>A transaction result which summarizes its outcome.</returns>
public async Task<TransactionResult> PurchaseAsync(OrderViewModel order)
{
// use the normalizer to validate the order.
OrderNormalizer orderNormalizer = new OrderNormalizer(this.ApplicationDomain, order);
order = await orderNormalizer.NormalizePurchaseSubscriptionOrderAsync();
// build the purchase line items.
List<PurchaseLineItem> purchaseLineItems = new List<PurchaseLineItem>();
foreach (var orderItem in order.Subscriptions)
{
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<IBusinessTransaction> subTransactions = new List<IBusinessTransaction>();
// 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
Order partnerCenterPurchaseOrder = this.BuildPartnerCenterOrder(lineItemsWithOffers);
var placeOrder = new PlaceOrder(
this.ApplicationDomain.PartnerCenterClient.Customers.ById(this.CustomerId),
partnerCenterPurchaseOrder);
subTransactions.Add(placeOrder);
// configure a transaction to save the new resulting subscriptions and purchases into persistence
var persistSubscriptionsAndPurchases = new PersistNewlyPurchasedSubscriptions(
this.CustomerId,
this.ApplicationDomain.CustomerSubscriptionsRepository,
this.ApplicationDomain.CustomerPurchasesRepository,
() => new Tuple<Order, IEnumerable<PurchaseLineItemWithOffer>>(placeOrder.Result, lineItemsWithOffers));
subTransactions.Add(persistSubscriptionsAndPurchases);
// configure a capture payment transaction and let it read the auth code from the payment authorization output
var capturePayment = new CapturePayment(this.PaymentGateway, () => paymentAuthorization.Result);
subTransactions.Add(capturePayment);
// build an aggregated transaction from the previous steps and execute it as a whole
await CommerceOperations.RunAggregatedTransaction(subTransactions);
return new TransactionResult(persistSubscriptionsAndPurchases.Result, DateTime.UtcNow);
}
/// <summary>
/// Purchases additional seats for an existing subscription the customer has already bought.
/// </summary>
/// <param name="order">The order to execute.</param>
/// <returns>A transaction result which summarizes its outcome.</returns>
public async Task<TransactionResult> PurchaseAdditionalSeatsAsync(OrderViewModel order)
{
// use the normalizer to validate the order.
OrderNormalizer orderNormalizer = new OrderNormalizer(this.ApplicationDomain, order);
order = await orderNormalizer.NormalizePurchaseAdditionalSeatsOrderAsync();
List<OrderSubscriptionItemViewModel> 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<IBusinessTransaction> subTransactions = new List<IBusinessTransaction>();
// configure a transaction to charge the payment gateway with the prorated rate
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,
new CustomerPurchaseEntity(CommerceOperationType.AdditionalSeatsPurchase, Guid.NewGuid().ToString(), this.CustomerId, subscriptionId, seatsToPurchase, proratedSeatCharge, rightNow)));
// add a capture payment to the transaction pipeline
subTransactions.Add(new CapturePayment(this.PaymentGateway, () => paymentAuthorization.Result));
// build an aggregated transaction from the previous steps and execute it as a whole
await CommerceOperations.RunAggregatedTransaction(subTransactions);
var additionalSeatsPurchaseResult = new TransactionResultLineItem(
subscriptionId,
partnerOfferId,
seatsToPurchase,
proratedSeatCharge,
seatsToPurchase * proratedSeatCharge);
return new TransactionResult(
new TransactionResultLineItem[] { additionalSeatsPurchaseResult },
rightNow);
}
/// <summary>
/// Renews an existing subscription for a customer.
/// </summary>
/// <param name="order">The order to execute.</param>
/// <returns>A transaction result which summarizes its outcome.</returns>
public async Task<TransactionResult> RenewSubscriptionAsync(OrderViewModel order)
{
// use the normalizer to validate the order.
OrderNormalizer orderNormalizer = new OrderNormalizer(this.ApplicationDomain, order);
order = await orderNormalizer.NormalizeRenewSubscriptionOrderAsync();
List<OrderSubscriptionItemViewModel> 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, Resources.Culture.NumberFormat.CurrencyDecimalDigits);
// retrieve the subscription from Partner Center
var subscriptionOperations = this.ApplicationDomain.PartnerCenterClient.Customers.ById(this.CustomerId).Subscriptions.ById(subscriptionId);
var partnerCenterSubscription = await subscriptionOperations.GetAsync();
// we will add up the transactions here
ICollection<IBusinessTransaction> subTransactions = new List<IBusinessTransaction>();
// configure a transaction to charge the payment gateway with the prorated rate
var paymentAuthorization = new AuthorizePayment(this.PaymentGateway);
subTransactions.Add(paymentAuthorization);
// add a renew subscription transaction to the pipeline
subTransactions.Add(new RenewSubscription(
subscriptionOperations,
partnerCenterSubscription));
DateTime rightNow = DateTime.UtcNow;
// 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, partnerOfferPrice, rightNow)));
// extend the expiry date by one year
subTransactions.Add(new UpdatePersistedSubscription(
this.ApplicationDomain.CustomerSubscriptionsRepository,
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));
// run the pipeline
await CommerceOperations.RunAggregatedTransaction(subTransactions);
var renewSubscriptionResult = new TransactionResultLineItem(
subscriptionId,
partnerOfferId,
partnerCenterSubscription.Quantity,
partnerOfferPrice,
totalCharge);
return new TransactionResult(
new TransactionResultLineItem[] { renewSubscriptionResult },
rightNow);
}
/// <summary>
/// Runs a given list of transactions as a whole.
/// </summary>
/// <param name="subTransactions">A collection of transactions to run.</param>
/// <returns>A task.</returns>
private static async Task RunAggregatedTransaction(IEnumerable<IBusinessTransaction> subTransactions)
{
// build an aggregated transaction from the given transactions
var aggregateTransaction = new SequentialAggregateTransaction(subTransactions);
try
{
// execute it
await aggregateTransaction.ExecuteAsync();
}
catch (Exception transactionFailure)
{
if (transactionFailure.IsFatal())
{
throw;
}
// roll back the whole transaction
await aggregateTransaction.RollbackAsync();
// report the error
throw;
}
}
/// <summary>
/// Binds each purchase line item with the partner offer it is requesting.
/// </summary>
/// <param name="purchaseLineItems">A collection of purchase line items.</param>
/// <returns>The requested association.</returns>
private async Task<IEnumerable<PurchaseLineItemWithOffer>> AssociateWithPartnerOffersAsync(IEnumerable<PurchaseLineItem> purchaseLineItems)
{
// retrieve all the partner offers to match against them
IEnumerable<PartnerOffer> allPartnerOffers = await this.ApplicationDomain.OffersRepository.RetrieveAsync();
ICollection<PurchaseLineItemWithOffer> lineItemToOfferAssociations = new List<PurchaseLineItemWithOffer>();
foreach (var lineItem in purchaseLineItems)
{
if (lineItem == null)
{
throw new ArgumentException("a line item is null");
}
PartnerOffer offerToPurchase = allPartnerOffers.Where(offer => offer.Id == lineItem.PartnerOfferId).FirstOrDefault();
// associate the line item with the partner offer
lineItemToOfferAssociations.Add(new PurchaseLineItemWithOffer(lineItem, offerToPurchase));
}
return lineItemToOfferAssociations;
}
/// <summary>
/// Builds a Microsoft Partner Center order from a list of purchase line items.
/// </summary>
/// <param name="purchaseLineItems">The purchase line items.</param>
/// <returns>The Partner Center Order.</returns>
private Order BuildPartnerCenterOrder(IEnumerable<PurchaseLineItemWithOffer> purchaseLineItems)
{
int lineItemNumber = 0;
ICollection<OrderLineItem> partnerCenterOrderLineItems = new List<OrderLineItem>();
// build the Partner Center order line items
foreach (var lineItem in purchaseLineItems)
{
// add the line items to the partner center order and calculate the price to charge
partnerCenterOrderLineItems.Add(new OrderLineItem()
{
OfferId = lineItem.PartnerOffer.MicrosoftOfferId,
Quantity = lineItem.PurchaseLineItem.Quantity,
LineItemNumber = lineItemNumber++
});
}
// bundle the order line items into a partner center order
return new Order()
{
ReferenceCustomerId = this.CustomerId,
LineItems = partnerCenterOrderLineItems
};
}
}
}

Просмотреть файл

@ -0,0 +1,170 @@
// -----------------------------------------------------------------------
// <copyright file="CustomerPurchasesRepository.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Exceptions;
using Models;
using WindowsAzure.Storage.Table;
/// <summary>
/// Encapsulates persistence for customer purchases.
/// </summary>
public class CustomerPurchasesRepository : DomainObject
{
/// <summary>
/// Initializes a new instance of the <see cref="CustomerPurchasesRepository"/> class.
/// </summary>
/// <param name="applicationDomain">An instance of the application domain.</param>
public CustomerPurchasesRepository(ApplicationDomain applicationDomain) : base(applicationDomain)
{
}
/// <summary>
/// Adds a new customer purchase into persistence.
/// </summary>
/// <param name="newCustomerPurchase">The new customer purchase persistence to add.</param>
/// <returns>The resulting customer purchase that got added.</returns>
public async Task<CustomerPurchaseEntity> AddAsync(CustomerPurchaseEntity newCustomerPurchase)
{
newCustomerPurchase.AssertNotNull(nameof(newCustomerPurchase));
CustomerPurchaseTableEntity customerPurchaseTableEntity = new CustomerPurchaseTableEntity(newCustomerPurchase.CustomerId, newCustomerPurchase.SubscriptionId)
{
SeatPrice = newCustomerPurchase.SeatPrice.ToString(CultureInfo.InvariantCulture),
SeatsBought = newCustomerPurchase.SeatsBought,
TransactionDate = newCustomerPurchase.TransactionDate,
PurchaseType = newCustomerPurchase.PurchaseType.ToString()
};
var customerPurchasesTable = await this.ApplicationDomain.AzureStorageService.GetCustomerPurchasesTableAsync();
var insertionResult = await customerPurchasesTable.ExecuteAsync(TableOperation.Insert(customerPurchaseTableEntity));
insertionResult.HttpStatusCode.AssertHttpResponseSuccess(ErrorCode.PersistenceFailure, "Failed to add customer purchase", insertionResult.Result);
newCustomerPurchase = new CustomerPurchaseEntity(
newCustomerPurchase.PurchaseType,
customerPurchaseTableEntity.RowKey,
newCustomerPurchase.CustomerId,
newCustomerPurchase.SubscriptionId,
newCustomerPurchase.SeatsBought,
newCustomerPurchase.SeatPrice,
newCustomerPurchase.TransactionDate);
return newCustomerPurchase;
}
/// <summary>
/// Removes a purchase entity from persistence.
/// </summary>
/// <param name="customerPurchaseToRemove">The customer purchase to remove.</param>
/// <returns>A task.</returns>
public async Task DeleteAsync(CustomerPurchaseEntity customerPurchaseToRemove)
{
customerPurchaseToRemove.AssertNotNull(nameof(customerPurchaseToRemove));
var customerPurchasesTable = await this.ApplicationDomain.AzureStorageService.GetCustomerPurchasesTableAsync();
var deletionResult = await customerPurchasesTable.ExecuteAsync(
TableOperation.Delete(new CustomerPurchaseTableEntity(customerPurchaseToRemove.CustomerId, customerPurchaseToRemove.SubscriptionId) { RowKey = customerPurchaseToRemove.Id, ETag = "*" }));
deletionResult.HttpStatusCode.AssertHttpResponseSuccess(ErrorCode.PersistenceFailure, "Failed to delete customer purchase", deletionResult.Result);
}
/// <summary>
/// Retrieves all purchases made by a customer from persistence.
/// </summary>
/// <param name="customerId">The customer ID.</param>
/// <returns>The customer's purchases.</returns>
public async Task<IEnumerable<CustomerPurchaseEntity>> RetrieveAsync(string customerId)
{
customerId.AssertNotEmpty(nameof(customerId));
var customerPurchasesTable = await this.ApplicationDomain.AzureStorageService.GetCustomerPurchasesTableAsync();
var getCustomerPurchasesQuery = new TableQuery<CustomerPurchaseTableEntity>().Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, customerId));
TableQuerySegment<CustomerPurchaseTableEntity> resultSegment = null;
ICollection<CustomerPurchaseEntity> customerPurchases = new List<CustomerPurchaseEntity>();
do
{
resultSegment = await customerPurchasesTable.ExecuteQuerySegmentedAsync(getCustomerPurchasesQuery, resultSegment?.ContinuationToken);
foreach (var customerPurchaseResult in resultSegment.AsEnumerable())
{
customerPurchases.Add(new CustomerPurchaseEntity(
(CommerceOperationType)Enum.Parse(typeof(CommerceOperationType), customerPurchaseResult.PurchaseType, true),
customerPurchaseResult.RowKey,
customerPurchaseResult.PartitionKey,
customerPurchaseResult.SubscriptionId,
customerPurchaseResult.SeatsBought,
decimal.Parse(customerPurchaseResult.SeatPrice, CultureInfo.CurrentCulture),
customerPurchaseResult.TransactionDate));
}
}
while (resultSegment.ContinuationToken != null);
return customerPurchases;
}
/// <summary>
/// An azure table entity that describes a customer purchase.
/// </summary>
private class CustomerPurchaseTableEntity : TableEntity
{
/// <summary>
/// Initializes a new instance of the <see cref="CustomerPurchaseTableEntity"/> class.
/// </summary>
public CustomerPurchaseTableEntity()
{
this.RowKey = Guid.NewGuid().ToString();
}
/// <summary>
/// Initializes a new instance of the <see cref="CustomerPurchaseTableEntity"/> class.
/// </summary>
/// <param name="customerId">The customer ID.</param>
/// <param name="subscriptionId">The subscription ID.</param>
public CustomerPurchaseTableEntity(string customerId, string subscriptionId)
{
this.PartitionKey = customerId;
this.RowKey = Guid.NewGuid().ToString();
this.SubscriptionId = subscriptionId;
}
/// <summary>
/// Gets or sets the subscription ID.
/// </summary>
public string SubscriptionId { get; set; }
/// <summary>
/// Gets or sets the commerce purchase type for this purchase item.
/// </summary>
public string PurchaseType { get; set; }
/// <summary>
/// Gets or sets the seat price.
/// </summary>
public string SeatPrice { get; set; }
/// <summary>
/// Gets or sets the number of seats bought.
/// </summary>
public int SeatsBought { get; set; }
/// <summary>
/// Gets or sets the transaction date.
/// </summary>
public DateTime TransactionDate { get; set; }
}
}
}

Просмотреть файл

@ -0,0 +1,140 @@
// -----------------------------------------------------------------------
// <copyright file="CustomerRegistrationRepository.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce
{
using System.Linq;
using System.Threading.Tasks;
using Exceptions;
using Models;
using Newtonsoft.Json;
using WindowsAzure.Storage.Table;
/// <summary>
/// Encapsulates persistence for customer registrations
/// </summary>
public class CustomerRegistrationRepository : DomainObject
{
/// <summary>
/// The partner customer key in the cache.
/// </summary>
private const string PartnerCustomerCacheKey = "PartnerCustomers";
/// <summary>
/// The Azure BLOB name for the partner customer details.
/// </summary>
private const string PartnerCustomerBlobName = "partnercustomers";
/// <summary>
/// Initializes a new instance of the <see cref="CustomerRegistrationRepository"/> class.
/// </summary>
/// <param name="applicationDomain">An instance of the application domain.</param>
public CustomerRegistrationRepository(ApplicationDomain applicationDomain) : base(applicationDomain)
{
}
/// <summary>
/// Adds a new customer registration details into persistence.
/// </summary>
/// <param name="customerRegistrationInfo">The new customer registration details to add.</param>
/// <returns>The resulting customer registration details that get added.</returns>
public async Task<CustomerViewModel> AddAsync(CustomerViewModel customerRegistrationInfo)
{
customerRegistrationInfo.AssertNotNull(nameof(customerRegistrationInfo));
var customerRegistrationTable = await this.ApplicationDomain.AzureStorageService.GetCustomerRegistrationTableAsync();
CustomerRegistrationTableEntity customerRegistrationTableEntity = new CustomerRegistrationTableEntity(customerRegistrationInfo);
var insertionResult = await customerRegistrationTable.ExecuteAsync(TableOperation.Insert(customerRegistrationTableEntity));
insertionResult.HttpStatusCode.AssertHttpResponseSuccess(ErrorCode.PersistenceFailure, "Failed to add customer registration details", insertionResult.Result);
return customerRegistrationInfo;
}
/// <summary>
/// Removes customer from persistence.
/// </summary>
/// <param name="customerId">Id of the customer to remove.</param>
/// <returns>A task.</returns>
public async Task DeleteAsync(string customerId)
{
customerId.AssertNotEmpty(nameof(customerId));
var customerRegistrationTable = await this.ApplicationDomain.AzureStorageService.GetCustomerRegistrationTableAsync();
var deletionResult = customerRegistrationTable.Execute(
TableOperation.Delete(new CustomerRegistrationTableEntity() { PartitionKey = customerId, RowKey = customerId, ETag = "*" }));
deletionResult.HttpStatusCode.AssertHttpResponseSuccess(ErrorCode.PersistenceFailure, "Failed to delete persisted customer registration info", deletionResult.Result);
}
/// <summary>
/// Retrieves customer registration details from persistence.
/// </summary>
/// <param name="customerGuid">The customer ID.</param>
/// <returns>The customer's registration details.</returns>
public async Task<CustomerViewModel> RetrieveAsync(string customerGuid)
{
customerGuid.AssertNotEmpty(nameof(customerGuid));
var customerRegistrationTable = await this.ApplicationDomain.AzureStorageService.GetCustomerRegistrationTableAsync();
string tableQueryFilter = TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, customerGuid);
var getCustomerOrdersQuery = new TableQuery<CustomerRegistrationTableEntity>().Where(tableQueryFilter);
TableQuerySegment<CustomerRegistrationTableEntity> resultSegment = null;
CustomerViewModel customerRegistrationInfo = null;
resultSegment = await customerRegistrationTable.ExecuteQuerySegmentedAsync<CustomerRegistrationTableEntity>(getCustomerOrdersQuery, resultSegment?.ContinuationToken);
do
{
resultSegment = await customerRegistrationTable.ExecuteQuerySegmentedAsync<CustomerRegistrationTableEntity>(getCustomerOrdersQuery, resultSegment?.ContinuationToken);
foreach (var customerResult in resultSegment.AsEnumerable())
{
if (customerResult.RowKey == customerGuid)
{
customerRegistrationInfo = JsonConvert.DeserializeObject<CustomerViewModel>(customerResult.CustomerRegistrationBlob);
}
}
}
while (resultSegment.ContinuationToken != null);
return customerRegistrationInfo;
}
/// <summary>
/// A azure table entity for customer registrations.
/// </summary>
private class CustomerRegistrationTableEntity : TableEntity
{
/// <summary>
/// Initializes a new instance of the <see cref="CustomerRegistrationTableEntity"/> class.
/// </summary>
public CustomerRegistrationTableEntity()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CustomerRegistrationTableEntity"/> class.
/// </summary>
/// <param name="customerRegistrationInfo">The customer registration entity</param>
public CustomerRegistrationTableEntity(CustomerViewModel customerRegistrationInfo)
{
this.RowKey = customerRegistrationInfo.MicrosoftId;
this.PartitionKey = customerRegistrationInfo.MicrosoftId;
this.CustomerRegistrationBlob = JsonConvert.SerializeObject(customerRegistrationInfo, Formatting.None);
}
/// <summary>
/// Gets or sets the blob which contains the customer registration details.
/// </summary>
public string CustomerRegistrationBlob { get; set; }
}
}
}

Просмотреть файл

@ -0,0 +1,160 @@
// -----------------------------------------------------------------------
// <copyright file="CustomerSubscriptionsRepository.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Exceptions;
using Models;
using WindowsAzure.Storage.Table;
/// <summary>
/// Manages persisting customer subscriptions.
/// </summary>
public class CustomerSubscriptionsRepository : DomainObject
{
/// <summary>
/// Initializes a new instance of the <see cref="CustomerSubscriptionsRepository"/> class.
/// </summary>
/// <param name="applicationDomain">An application domain instance.</param>
public CustomerSubscriptionsRepository(ApplicationDomain applicationDomain) : base(applicationDomain)
{
}
/// <summary>
/// Adds a new customer subscription.
/// </summary>
/// <param name="newCustomerSubscription">The new subscription information.</param>
/// <returns>The just added customer subscription.</returns>
public async Task<CustomerSubscriptionEntity> AddAsync(CustomerSubscriptionEntity newCustomerSubscription)
{
newCustomerSubscription.AssertNotNull(nameof(newCustomerSubscription));
CustomerSubscriptionTableEntity customerSubscriptionTableEntity = new CustomerSubscriptionTableEntity(newCustomerSubscription.CustomerId, newCustomerSubscription.SubscriptionId)
{
PartnerOfferId = newCustomerSubscription.PartnerOfferId,
ExpiryDate = newCustomerSubscription.ExpiryDate
};
var customerSubscriptionsTable = await this.ApplicationDomain.AzureStorageService.GetCustomerSubscriptionsTableAsync();
var insertionResult = await customerSubscriptionsTable.ExecuteAsync(TableOperation.Insert(customerSubscriptionTableEntity));
insertionResult.HttpStatusCode.AssertHttpResponseSuccess(ErrorCode.PersistenceFailure, "Failed to add customer subscription", insertionResult.Result);
return newCustomerSubscription;
}
/// <summary>
/// Removes a customer subscription.
/// </summary>
/// <param name="customerSubscriptionToRemove">The customer subscription to remove.</param>
/// <returns>A task.</returns>
public async Task DeleteAsync(CustomerSubscriptionEntity customerSubscriptionToRemove)
{
customerSubscriptionToRemove.AssertNotNull(nameof(customerSubscriptionToRemove));
var customerSubscriptionsTable = await this.ApplicationDomain.AzureStorageService.GetCustomerSubscriptionsTableAsync();
var deletionResult = await customerSubscriptionsTable.ExecuteAsync(TableOperation.Delete(new CustomerSubscriptionTableEntity(customerSubscriptionToRemove.CustomerId, customerSubscriptionToRemove.SubscriptionId) { ETag = "*" }));
deletionResult.HttpStatusCode.AssertHttpResponseSuccess(ErrorCode.PersistenceFailure, "Failed to remove customer subscription", deletionResult.Result);
}
/// <summary>
/// Updates a customer subscription.
/// </summary>
/// <param name="customerSubscriptionUpdates">The customer subscription updates.</param>
/// <returns>The updated customer subscription.</returns>
public async Task<CustomerSubscriptionEntity> UpdateAsync(CustomerSubscriptionEntity customerSubscriptionUpdates)
{
customerSubscriptionUpdates.AssertNotNull(nameof(customerSubscriptionUpdates));
var updateSubscriptionOperation = TableOperation.Replace(new CustomerSubscriptionTableEntity(customerSubscriptionUpdates.CustomerId, customerSubscriptionUpdates.SubscriptionId)
{
ExpiryDate = customerSubscriptionUpdates.ExpiryDate,
PartnerOfferId = customerSubscriptionUpdates.PartnerOfferId,
ETag = "*"
});
var customerSubscriptionsTable = await this.ApplicationDomain.AzureStorageService.GetCustomerSubscriptionsTableAsync();
var updateResult = await customerSubscriptionsTable.ExecuteAsync(updateSubscriptionOperation);
updateResult.HttpStatusCode.AssertHttpResponseSuccess(ErrorCode.PersistenceFailure, "Failed to update customer subscription", updateResult.Result);
return customerSubscriptionUpdates;
}
/// <summary>
/// Retrieves all customer subscriptions.
/// </summary>
/// <param name="customerId">The ID of the customer who owns the subscriptions.</param>
/// <returns>A list of customer subscriptions.</returns>
public async Task<IEnumerable<CustomerSubscriptionEntity>> RetrieveAsync(string customerId)
{
customerId.AssertNotEmpty(nameof(customerId));
var customerSubscriptionsTable = await this.ApplicationDomain.AzureStorageService.GetCustomerSubscriptionsTableAsync();
var getCustomerSubscriptionsQuery = new TableQuery<CustomerSubscriptionTableEntity>().Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, customerId));
TableQuerySegment<CustomerSubscriptionTableEntity> resultSegment = null;
ICollection<CustomerSubscriptionEntity> customerSubscriptions = new List<CustomerSubscriptionEntity>();
do
{
resultSegment = await customerSubscriptionsTable.ExecuteQuerySegmentedAsync<CustomerSubscriptionTableEntity>(getCustomerSubscriptionsQuery, resultSegment?.ContinuationToken);
foreach (var customerSubscriptionResult in resultSegment.AsEnumerable())
{
customerSubscriptions.Add(new CustomerSubscriptionEntity(
customerSubscriptionResult.PartitionKey,
customerSubscriptionResult.RowKey,
customerSubscriptionResult.PartnerOfferId,
customerSubscriptionResult.ExpiryDate));
}
}
while (resultSegment.ContinuationToken != null);
return customerSubscriptions;
}
/// <summary>
/// A azure table entity for customer subscriptions.
/// </summary>
private class CustomerSubscriptionTableEntity : TableEntity
{
/// <summary>
/// Initializes a new instance of the <see cref="CustomerSubscriptionTableEntity"/> class.
/// </summary>
public CustomerSubscriptionTableEntity()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CustomerSubscriptionTableEntity"/> class.
/// </summary>
/// <param name="customerId">The customer ID.</param>
/// <param name="subscriptionId">The subscription ID.</param>
public CustomerSubscriptionTableEntity(string customerId, string subscriptionId)
{
this.PartitionKey = customerId;
this.RowKey = subscriptionId;
}
/// <summary>
/// Gets or sets the partner offer ID.
/// </summary>
public string PartnerOfferId { get; set; }
/// <summary>
/// Gets or sets the expiry date.
/// </summary>
public DateTime ExpiryDate { get; set; }
}
}
}

Просмотреть файл

@ -0,0 +1,48 @@
// -----------------------------------------------------------------------
// <copyright file="ICommerceOperations.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce
{
using System.Threading.Tasks;
using Models;
/// <summary>
/// A contract for components implementing commerce operations.
/// </summary>
public interface ICommerceOperations
{
/// <summary>
/// Gets the customer ID who owns the transaction.
/// </summary>
string CustomerId { get; }
/// <summary>
/// Gets the payment gateway used to process payments.
/// </summary>
IPaymentGateway PaymentGateway { get; }
/// <summary>
/// Purchases one or more partner offers.
/// </summary>
/// <param name="order">The order to execute.</param>
/// <returns>A transaction result which summarizes its outcome.</returns>
Task<TransactionResult> PurchaseAsync(OrderViewModel order);
/// <summary>
/// Purchases additional seats for an existing subscription the customer has already bought.
/// </summary>
/// <param name="order">The order to execute.</param>
/// <returns>A transaction result which summarizes its outcome.</returns>
Task<TransactionResult> PurchaseAdditionalSeatsAsync(OrderViewModel order);
/// <summary>
/// Renews an existing subscription for a customer.
/// </summary>
/// <param name="order">The order to execute.</param>
/// <returns>A transaction result which summarizes its outcome.</returns>
Task<TransactionResult> RenewSubscriptionAsync(OrderViewModel order);
}
}

Просмотреть файл

@ -0,0 +1,70 @@
// -----------------------------------------------------------------------
// <copyright file="IPaymentGateway.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce
{
using System.Threading.Tasks;
using Models;
/// <summary>
/// The payment gateway contract. Implement this interface to provide payment capabilities.
/// </summary>
public interface IPaymentGateway
{
/// <summary>
/// Executes a payment.
/// </summary>
/// <returns>An Authorization code.</returns>
Task<string> ExecutePaymentAsync();
/// <summary>
/// Finalizes an authorized payment.
/// </summary>
/// <param name="authorizationCode">The authorization code for the payment to capture.</param>
/// <returns>A task.</returns>
Task CaptureAsync(string authorizationCode);
/// <summary>
/// Voids an authorized payment.
/// </summary>
/// <param name="authorizationCode">The authorization code for the payment to void.</param>
/// <returns>a Task</returns>
Task VoidAsync(string authorizationCode);
/// <summary>
/// Generates the Payment gateway Url where actual payment collection is done.
/// </summary>
/// <param name="returnUrl">Application return Url.</param>
/// <param name="order">Order information.</param>
/// <returns>The payment gateway url.</returns>
Task<string> GeneratePaymentUriAsync(string returnUrl, OrderViewModel order);
/// <summary>
/// Retrieves the order details maintained for the payment gateway.
/// </summary>
/// <param name="payerId">The Payer Id.</param>
/// <param name="paymentId">The Payment Id.</param>
/// <param name="orderId">The Order Id.</param>
/// <param name="customerId">The Customer Id.</param>
/// <returns>The order associated with this payment transaction.</returns>
Task<OrderViewModel> GetOrderDetailsFromPaymentAsync(string payerId, string paymentId, string orderId, string customerId);
/// <summary>
/// validate payment configuration.
/// </summary>
/// <param name="paymentConfiguration">The paymentConfiguration contains all the payment configuration data.</param>
void ValidateConfiguration(PaymentConfiguration paymentConfiguration);
/// <summary>
/// creates web experience profile.
/// </summary>
/// <param name="paymentConfig">The payment configuration</param>
/// <param name="brandConfig">The brand configuration</param>
/// <param name="countryIso2Code">The country code</param>
/// <returns>returns web profile id</returns>
string CreateWebExperienceProfile(PaymentConfiguration paymentConfig, BrandingConfiguration brandConfig, string countryIso2Code);
}
}

Просмотреть файл

@ -0,0 +1,258 @@
// -----------------------------------------------------------------------
// <copyright file="OrderNormalizer.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Exceptions;
using Models;
/// <summary>
/// Implements the normalizers for orders.
/// </summary>
public class OrderNormalizer : DomainObject
{
/// <summary>
/// Initializes a new instance of the <see cref="OrderNormalizer"/> class.
/// </summary>
/// <param name="applicationDomain">An application domain instance.</param>
/// <param name="order">The order which will be normalized.</param>
public OrderNormalizer(ApplicationDomain applicationDomain, OrderViewModel order) : base(applicationDomain)
{
order.AssertNotNull(nameof(order));
this.Order = order;
}
/// <summary>
/// Gets the Order instance.
/// </summary>
public OrderViewModel Order { get; private set; }
/// <summary>
/// Normalizes an order to renew a subscription.
/// </summary>
/// <returns>Normalized order.</returns>
public async Task<OrderViewModel> 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,
OrderId = order.OrderId,
OperationType = order.OperationType
};
order.Subscriptions.AssertNotNull(nameof(order.Subscriptions));
List<OrderSubscriptionItemViewModel> 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<OrderSubscriptionItemViewModel> resultOrderSubscriptions = new List<OrderSubscriptionItemViewModel>();
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);
}
/// <summary>
/// Normalizes an order to purchase net new subscriptions.
/// </summary>
/// <returns>Normalized order.</returns>
public async Task<OrderViewModel> 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,
OrderId = order.OrderId,
OperationType = order.OperationType
};
order.Subscriptions.AssertNotNull(nameof(order.Subscriptions));
List<OrderSubscriptionItemViewModel> orderSubscriptions = order.Subscriptions.ToList();
if (orderSubscriptions.Count < 1)
{
throw new Exception(Resources.NotEnoughItemsInOrderErrorMessage);
}
// retrieve all the partner offers to match against them
IEnumerable<PartnerOffer> allPartnerOffers = await ApplicationDomain.Instance.OffersRepository.RetrieveAsync();
List<OrderSubscriptionItemViewModel> resultOrderSubscriptions = new List<OrderSubscriptionItemViewModel>();
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);
}
/// <summary>
/// Normalizes an order to add seats to a subscription.
/// </summary>
/// <returns>Normalized order.</returns>
public async Task<OrderViewModel> 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,
OrderId = order.OrderId,
OperationType = order.OperationType
};
order.Subscriptions.AssertNotNull(nameof(order.Subscriptions));
List<OrderSubscriptionItemViewModel> 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), Resources.Culture.NumberFormat.CurrencyDecimalDigits);
decimal totalCharge = Math.Round(proratedSeatCharge * seatsToPurchase, Resources.Culture.NumberFormat.CurrencyDecimalDigits);
List<OrderSubscriptionItemViewModel> resultOrderSubscriptions = new List<OrderSubscriptionItemViewModel>();
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);
}
/// <summary>
/// Retrieves a customer subscription from persistence.
/// </summary>
/// <param name="subscriptionId">The subscription ID.</param>
/// <param name="customerId">The customer ID.</param>
/// <returns>The matching subscription.</returns>
private async Task<CustomerSubscriptionEntity> 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;
}
}
}

Просмотреть файл

@ -0,0 +1,136 @@
// -----------------------------------------------------------------------
// <copyright file="OrdersRepository.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce
{
using System;
using System.Linq;
using System.Threading.Tasks;
using Exceptions;
using Models;
using Newtonsoft.Json;
using WindowsAzure.Storage.Table;
/// <summary>
/// Encapsulates persistence for orders during customer purchases.
/// </summary>
public class OrdersRepository : DomainObject
{
/// <summary>
/// Initializes a new instance of the <see cref="OrdersRepository"/> class.
/// </summary>
/// <param name="applicationDomain">An instance of the application domain.</param>
public OrdersRepository(ApplicationDomain applicationDomain) : base(applicationDomain)
{
}
/// <summary>
/// Adds a new order into persistence.
/// </summary>
/// <param name="newOrder">The new customer order to add.</param>
/// <returns>The resulting customer order that got added.</returns>
public async Task<OrderViewModel> AddAsync(OrderViewModel newOrder)
{
newOrder.AssertNotNull(nameof(newOrder));
var customerOrdersTable = await this.ApplicationDomain.AzureStorageService.GetCustomerOrdersTableAsync();
CustomerOrderTableEntity orderEntity = new CustomerOrderTableEntity(newOrder);
var insertionResult = await customerOrdersTable.ExecuteAsync(TableOperation.Insert(orderEntity));
insertionResult.HttpStatusCode.AssertHttpResponseSuccess(ErrorCode.PersistenceFailure, "Failed to add customer order", insertionResult.Result);
return newOrder;
}
/// <summary>
/// Removes an order from persistence.
/// </summary>
/// <param name="orderId">Id of the order to remove.</param>
/// <param name="customerId">Id of the customer whose order to remove.</param>
/// <returns>A task.</returns>
public async Task DeleteAsync(string orderId, string customerId)
{
orderId.AssertNotEmpty(nameof(orderId));
customerId.AssertNotEmpty(nameof(customerId));
var customerOrdersTable = await this.ApplicationDomain.AzureStorageService.GetCustomerOrdersTableAsync();
var deletionResult = await customerOrdersTable.ExecuteAsync(
TableOperation.Delete(new CustomerOrderTableEntity() { PartitionKey = customerId, RowKey = orderId, ETag = "*" }));
deletionResult.HttpStatusCode.AssertHttpResponseSuccess(ErrorCode.PersistenceFailure, "Failed to delete customer order", deletionResult.Result);
}
/// <summary>
/// Retrieves specific order made by a customer from persistence.
/// </summary>
/// <param name="orderId">The order ID.</param>
/// <param name="customerId">The customer ID.</param>
/// <returns>The customer's order.</returns>
public async Task<OrderViewModel> RetrieveAsync(string orderId, string customerId)
{
orderId.AssertNotEmpty(nameof(orderId));
customerId.AssertNotEmpty(nameof(customerId));
var customerOrdersTable = await this.ApplicationDomain.AzureStorageService.GetCustomerOrdersTableAsync();
string tableQueryFilter = TableQuery.CombineFilters(
TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, customerId),
TableOperators.And,
TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, orderId));
var getCustomerOrdersQuery = new TableQuery<CustomerOrderTableEntity>().Where(tableQueryFilter);
TableQuerySegment<CustomerOrderTableEntity> resultSegment = null;
OrderViewModel customerOrder = null;
do
{
resultSegment = await customerOrdersTable.ExecuteQuerySegmentedAsync<CustomerOrderTableEntity>(getCustomerOrdersQuery, resultSegment?.ContinuationToken);
foreach (var orderResult in resultSegment.AsEnumerable())
{
if (orderResult.RowKey == orderId)
{
customerOrder = JsonConvert.DeserializeObject<OrderViewModel>(orderResult.OrderBlob);
}
}
}
while (resultSegment.ContinuationToken != null);
return customerOrder;
}
/// <summary>
/// An azure table entity that describes a customer order.
/// </summary>
private class CustomerOrderTableEntity : TableEntity
{
/// <summary>
/// Initializes a new instance of the <see cref="CustomerOrderTableEntity"/> class.
/// </summary>
public CustomerOrderTableEntity()
{
this.RowKey = Guid.NewGuid().ToString();
}
/// <summary>
/// Initializes a new instance of the <see cref="CustomerOrderTableEntity"/> class.
/// </summary>
/// <param name="order">The order details.</param>
public CustomerOrderTableEntity(OrderViewModel order)
{
this.PartitionKey = order.CustomerId;
this.RowKey = order.OrderId;
this.OrderBlob = JsonConvert.SerializeObject(order, Formatting.None);
}
/// <summary>
/// Gets or sets the blob which contains the order details.
/// </summary>
public string OrderBlob { get; set; }
}
}
}

Просмотреть файл

@ -0,0 +1,136 @@
// -----------------------------------------------------------------------
// <copyright file="PaymentConfigurationRepository.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic
{
using System.Linq;
using System.Threading.Tasks;
using Exceptions;
using Models;
using Newtonsoft.Json;
using WindowsAzure.Storage.Blob;
/// <summary>
/// Manages persistence for payment configuration options.
/// </summary>
public class PaymentConfigurationRepository : DomainObject
{
/// <summary>
/// The payment configuration key in the cache.
/// </summary>
private const string PaymentConfigurationCacheKey = "PaymentConfiguration";
/// <summary>
/// The Azure BLOB name for the portal payment configuration.
/// </summary>
private const string PaymentConfigurationBlobName = "paymentconfiguration";
/// <summary>
/// The supported payment modes. We can move this to web.config is needed later.
/// </summary>
private readonly string[] supportedPaymentModes = { "sandbox", "live" };
/// <summary>
/// Initializes a new instance of the <see cref="PaymentConfigurationRepository"/> class.
/// </summary>
/// <param name="applicationDomain">An application domain instance.</param>
public PaymentConfigurationRepository(ApplicationDomain applicationDomain) : base(applicationDomain)
{
}
/// <summary>
/// Indicates whether the payment configuration has been set or not.
/// </summary>
/// <returns>True if configured, false otherwise.</returns>
public async Task<bool> IsConfiguredAsync()
{
var paymentConfigurationBlob = await this.GetPaymentConfigurationBlob();
return await paymentConfigurationBlob.ExistsAsync();
}
/// <summary>
/// Retrieves the payment configuration from persistence.
/// </summary>
/// <returns>The payment configuration.</returns>
public async Task<PaymentConfiguration> RetrieveAsync()
{
var paymentConfiguration = await this.ApplicationDomain.CachingService
.FetchAsync<PaymentConfiguration>(PaymentConfigurationRepository.PaymentConfigurationCacheKey);
if (paymentConfiguration == null)
{
var paymentConfigurationBlob = await this.GetPaymentConfigurationBlob();
paymentConfiguration = new PaymentConfiguration();
if (await paymentConfigurationBlob.ExistsAsync())
{
paymentConfiguration = JsonConvert.DeserializeObject<PaymentConfiguration>(await paymentConfigurationBlob.DownloadTextAsync());
await this.NormalizeAsync(paymentConfiguration);
// cache the payment configuration
await this.ApplicationDomain.CachingService.StoreAsync<PaymentConfiguration>(
PaymentConfigurationRepository.PaymentConfigurationCacheKey,
paymentConfiguration);
}
}
return paymentConfiguration;
}
/// <summary>
/// Updates the payment configuration.
/// </summary>
/// <param name="newPaymentConfiguration">The new payment configuration.</param>
/// <returns>The updated payment configuration.</returns>
public async Task<PaymentConfiguration> UpdateAsync(PaymentConfiguration newPaymentConfiguration)
{
newPaymentConfiguration.AssertNotNull(nameof(newPaymentConfiguration));
await this.NormalizeAsync(newPaymentConfiguration);
var paymentConfigurationBlob = await this.GetPaymentConfigurationBlob();
await paymentConfigurationBlob.UploadTextAsync(JsonConvert.SerializeObject(newPaymentConfiguration));
// invalidate the cache, we do not update it to avoid race condition between web instances
await this.ApplicationDomain.CachingService.ClearAsync(PaymentConfigurationRepository.PaymentConfigurationCacheKey);
return newPaymentConfiguration;
}
/// <summary>
/// Applies business rules to <see cref="PaymentConfiguration"/> instances.
/// </summary>
/// <param name="paymentConfiguration">A payment configuration instance.</param>
/// <returns>A task.</returns>
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(Resources.InvalidPaymentModeErrorMessage);
}
await Task.FromResult(0);
}
/// <summary>
/// Retrieves the portal payment configuration BLOB reference.
/// </summary>
/// <returns>The portal payment configuration BLOB.</returns>
private async Task<CloudBlockBlob> GetPaymentConfigurationBlob()
{
var portalAssetsBlobContainer = await this.ApplicationDomain.AzureStorageService.GetPrivateCustomerPortalAssetsBlobContainerAsync();
return portalAssetsBlobContainer.GetBlockBlobReference(PaymentConfigurationRepository.PaymentConfigurationBlobName);
}
}
}

Просмотреть файл

@ -0,0 +1,544 @@
// -----------------------------------------------------------------------
// <copyright file="PayPalGateway.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce.PaymentGateways
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Exceptions;
using Models;
using PayPal;
using PayPal.Api;
/// <summary>
/// PayPal payment gateway implementation.
/// </summary>
public class PayPalGateway : DomainObject, IPaymentGateway
{
/// <summary>
/// Maintains the description for this payment.
/// </summary>
private readonly string paymentDescription;
/// <summary>
/// Maintains the payer id for the payment gateway.
/// </summary>
private string payerId;
/// <summary>
/// Maintains the payment id for the payment gateway.
/// </summary>
private string paymentId;
/// <summary>
/// Initializes a new instance of the <see cref="PayPalGateway" /> class.
/// </summary>
/// <param name="applicationDomain">The ApplicationDomain</param>
/// <param name="description">The description which will be added to the Payment Card authorization call.</param>
public PayPalGateway(ApplicationDomain applicationDomain, string description) : base(applicationDomain)
{
description.AssertNotEmpty(nameof(description));
this.paymentDescription = description;
this.payerId = string.Empty;
this.paymentId = string.Empty;
}
/// <summary>
/// Validates payment configuration.
/// </summary>
/// <param name="paymentConfig">The Payment configuration.</param>
public void ValidateConfiguration(PaymentConfiguration paymentConfig)
{
string[] supportedPaymentModes = { "sandbox", "live" };
paymentConfig.AssertNotNull(nameof(paymentConfig));
paymentConfig.ClientId.AssertNotEmpty(nameof(paymentConfig.ClientId));
paymentConfig.ClientSecret.AssertNotEmpty(nameof(paymentConfig.ClientSecret));
paymentConfig.AccountType.AssertNotEmpty(nameof(paymentConfig.AccountType));
if (!supportedPaymentModes.Contains(paymentConfig.AccountType))
{
throw new PartnerDomainException(Resources.InvalidPaymentModeErrorMessage);
}
try
{
Dictionary<string, string> configMap = new Dictionary<string, string>();
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);
}
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.Equals("invalid_client", StringComparison.InvariantCultureIgnoreCase))
{
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);
}
}
/// <summary>
/// Creates Web Experience profile using portal branding and payment configuration.
/// </summary>
/// <param name="paymentConfig">The Payment configuration.</param>
/// <param name="brandConfig">The branding configuration.</param>
/// <param name="countryIso2Code">The locale code used by the web experience profile. Example-US.</param>
/// <returns>The created web experience profile id.</returns>
public string CreateWebExperienceProfile(PaymentConfiguration paymentConfig, BrandingConfiguration brandConfig, string countryIso2Code)
{
try
{
Dictionary<string, string> configMap = new Dictionary<string, string>
{
{ "clientId", paymentConfig.ClientId },
{ "clientSecret", paymentConfig.ClientSecret },
{ "mode", paymentConfig.AccountType },
{ "connectionTimeout", "120000" }
};
string accessToken = new OAuthTokenCredential(configMap).GetAccessToken();
var apiContext = new APIContext(accessToken)
{
Config = configMap
};
// Pickup logo & brand name from branding configuration.
// create the web experience profile.
var profile = new WebProfile
{
name = Guid.NewGuid().ToString(),
presentation = new Presentation
{
brand_name = brandConfig.OrganizationName,
logo_image = brandConfig.HeaderImage?.ToString(),
locale_code = countryIso2Code
},
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);
// Now that new experience profile is created hence delete the older one.
if (!string.IsNullOrWhiteSpace(paymentConfig.WebExperienceProfileId))
{
try
{
WebProfile existingWebProfile = WebProfile.Get(apiContext, paymentConfig.WebExperienceProfileId);
existingWebProfile.Delete(apiContext);
}
catch
{
}
}
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.Equals("invalid_client", StringComparison.InvariantCultureIgnoreCase))
{
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);
}
}
/// <summary>
/// Creates a payment transaction and returns the PayPal generated payment URL.
/// </summary>
/// <param name="returnUrl">The redirect url for PayPal callback to web store portal.</param>
/// <param name="order">The order details for which payment needs to be made.</param>
/// <returns>Payment URL from PayPal.</returns>
public async Task<string> GeneratePaymentUriAsync(string returnUrl, OrderViewModel order)
{
string paypalRedirectUrl = string.Empty;
returnUrl.AssertNotEmpty(nameof(returnUrl));
order.AssertNotNull(nameof(order));
APIContext apiContext = await this.GetAPIContextAsync().ConfigureAwait(false);
decimal paymentTotal = 0;
// PayPal wouldnt manage decimal points for few countries (example Hungary & Japan).
string moneyFixedPointFormat = (Resources.Culture.NumberFormat.CurrencyDecimalDigits == 0) ? "F0" : "F";
// Create itemlist and add item objects to it.
var itemList = new ItemList() { items = new List<Item>() };
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(moneyFixedPointFormat, CultureInfo.InvariantCulture),
quantity = subscriptionItem.Quantity.ToString(CultureInfo.InvariantCulture)
});
paymentTotal += Math.Round(subscriptionItem.Quantity * subscriptionItem.SeatPrice, Resources.Culture.NumberFormat.CurrencyDecimalDigits);
}
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<Transaction>()
{
new Transaction()
{
description = this.paymentDescription,
custom = string.Format(CultureInfo.InvariantCulture, "{0}#{1}", order.CustomerId, order.OperationType.ToString()),
item_list = itemList,
amount = new Amount()
{
currency = this.ApplicationDomain.PortalLocalization.CurrencyCode,
total = paymentTotal.ToString(moneyFixedPointFormat, CultureInfo.InvariantCulture)
}
}
},
redirect_urls = new RedirectUrls()
{
return_url = returnUrl + "&payment=success",
cancel_url = returnUrl + "&payment=failure"
}
};
System.Diagnostics.Debug.WriteLine("Total Amount:" + paymentTotal.ToString("F", Resources.Culture));
try
{
// 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.Trim().Equals("approval_url", StringComparison.InvariantCultureIgnoreCase))
{
paypalRedirectUrl = lnk.href;
}
}
return paypalRedirectUrl;
}
catch (PayPalException ex)
{
this.ParsePayPalException(ex);
}
return string.Empty;
}
/// <summary>
/// Executes a PayPal payment.
/// </summary>
/// <returns>Capture string id.</returns>
public async Task<string> ExecutePaymentAsync()
{
APIContext apiContext = await this.GetAPIContextAsync().ConfigureAwait(false);
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.Equals("approved", StringComparison.InvariantCultureIgnoreCase))
{
string authorizationCode = paymentResult.transactions[0].related_resources[0].authorization.id;
return authorizationCode;
}
}
catch (PayPalException ex)
{
this.ParsePayPalException(ex);
}
return string.Empty;
}
/// <summary>
/// Finalizes an authorized payment with PayPal.
/// </summary>
/// <param name="authorizationCode">The authorization code for the payment to capture.</param>
/// <returns>A task.</returns>
public async Task CaptureAsync(string authorizationCode)
{
string authorizationCurrency;
string authorizationAmount;
Authorization cardAuthorization = null;
authorizationCode.AssertNotEmpty(nameof(authorizationCode));
APIContext apiContext = await this.GetAPIContextAsync().ConfigureAwait(false);
// given the authorizationId. Lookup the authorization to find the amount.
try
{
cardAuthorization = Authorization.Get(apiContext, authorizationCode);
authorizationCurrency = cardAuthorization.amount.currency;
authorizationAmount = cardAuthorization.amount.total;
// Setting 'is_final_capture' to true, all remaining funds held by the authorization will be released from the funding instrument.
var capture = new Capture()
{
amount = new Amount()
{
currency = authorizationCurrency,
total = authorizationAmount
},
is_final_capture = true
};
var responseCapture = cardAuthorization.Capture(apiContext, capture);
}
catch (PayPalException ex)
{
this.ParsePayPalException(ex);
}
}
/// <summary>
/// Voids an authorized payment with PayPal.
/// </summary>
/// <param name="authorizationCode">The authorization code for the payment to void.</param>
/// <returns>a Task</returns>
public async Task VoidAsync(string authorizationCode)
{
authorizationCode.AssertNotEmpty(nameof(authorizationCode));
// given the authorizationId string... Lookup the authorization to void it.
try
{
APIContext apiContext = await this.GetAPIContextAsync().ConfigureAwait(false);
Authorization cardAuthorization = Authorization.Get(apiContext, authorizationCode);
cardAuthorization.Void(apiContext);
}
catch (PayPalException ex)
{
this.ParsePayPalException(ex);
}
}
/// <summary>
/// Retrieves the order details maintained for the payment gateway.
/// </summary>
/// <param name="payerId">The Payer Id.</param>
/// <param name="paymentId">The Payment Id.</param>
/// <param name="orderId">The Order Id.</param>
/// <param name="customerId">The Customer Id.</param>
/// <returns>The order associated with this payment transaction.</returns>
public async Task<OrderViewModel> GetOrderDetailsFromPaymentAsync(string payerId, string paymentId, string orderId, string customerId)
{
// this payment gateway ignores orderId & customerId.
payerId.AssertNotEmpty(nameof(payerId));
paymentId.AssertNotEmpty(nameof(paymentId));
this.payerId = payerId;
this.paymentId = paymentId;
return await GetOrderDetails().ConfigureAwait(false);
}
/// <summary>
/// Retrieves the Order from a payment transaction.
/// </summary>
/// <returns>The Order for which payment was made.</returns>
private async Task<OrderViewModel> GetOrderDetails()
{
OrderViewModel orderFromPayment = null;
APIContext apiContext = await GetAPIContextAsync().ConfigureAwait(false);
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<OrderSubscriptionItemViewModel> orderSubscriptions = new List<OrderSubscriptionItemViewModel>();
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)
});
}
}
orderFromPayment.Subscriptions = orderSubscriptions;
}
catch (PayPalException ex)
{
this.ParsePayPalException(ex);
}
return await Task.FromResult(orderFromPayment);
}
/// <summary>
/// Retrieves the API Context for PayPal.
/// </summary>
/// <returns>PayPal APIContext</returns>
private async Task<APIContext> 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.
//// See more - https://github.com/paypal/PayPal-NET-SDK/blob/develop/Source/SDK/Api/OAuthTokenCredential.cs
// Before getAPIContext ... set up PayPal configuration. This is an expensive call which can benefit from caching.
PaymentConfiguration paymentConfig = await ApplicationDomain.Instance.PaymentConfigurationRepository.RetrieveAsync().ConfigureAwait(false);
Dictionary<string, string> configMap = new Dictionary<string, string>
{
{ "clientId", paymentConfig.ClientId },
{ "clientSecret", paymentConfig.ClientSecret },
{ "mode", paymentConfig.AccountType },
{ "WebExperienceProfileId", paymentConfig.WebExperienceProfileId },
{ "connectionTimeout", "120000" }
};
string accessToken = new OAuthTokenCredential(configMap).GetAccessToken();
var apiContext = new APIContext(accessToken)
{
Config = configMap
};
return apiContext;
}
/// <summary>
/// Throws PartnerDomainException by parsing PayPal exception.
/// </summary>
/// <param name="ex">Exceptions from PayPal SDK.</param>
private void ParsePayPalException(PayPalException ex)
{
if (ex is PaymentsException)
{
PaymentsException pe = ex as PaymentsException;
// 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(CultureInfo.InvariantCulture);
if (errorName == null || errorName.Length < 1)
{
errorString.Append(pe.Details.message);
throw new PartnerDomainException(ErrorCode.PaymentGatewayFailure).AddDetail("ErrorMessage", errorString.ToString());
}
else if (errorName.Contains("UNKNOWN_ERROR"))
{
throw new PartnerDomainException(ErrorCode.PaymentGatewayPaymentError);
}
else if (errorName.Contains("VALIDATION") && pe.Details.details != null)
{
// Check if there are sub collection details and build error string.
errorString.Append("[");
foreach (ErrorDetails errorDetails in pe.Details.details)
{
// removing extrataneous information.
string errorField = errorDetails.field;
if (errorField.Contains("payer.funding_instruments[0]."))
{
errorField = errorField.Replace("payer.funding_instruments[0].", string.Empty).ToString(CultureInfo.InvariantCulture);
}
errorString.AppendFormat("{0} - {1},", errorField, errorDetails.issue);
}
errorString.Replace(',', ']', errorString.Length - 2, 2); // remove the last comma and replace it with ].
}
else
{
errorString.Append(Resources.PayPalUnableToProcessPayment);
}
}
throw new PartnerDomainException(ErrorCode.PaymentGatewayFailure).AddDetail("ErrorMessage", errorString.ToString());
}
if (ex is IdentityException)
{
// ideally this shouldn't be raised from customer experience calls.
// can occur when admin has generated a new secret for an existing app id in PayPal but didnt update portal payment configuration.
throw new PartnerDomainException(ErrorCode.PaymentGatewayIdentityFailureDuringPayment).AddDetail("ErrorMessage", Resources.PaymentGatewayIdentityFailureDuringPayment);
}
// few PayPalException types contain meaningfull exception information only in InnerException.
if (ex is PayPalException && ex.InnerException != null)
{
throw new PartnerDomainException(ErrorCode.PaymentGatewayFailure).AddDetail("ErrorMessage", ex.InnerException.Message);
}
else
{
throw new PartnerDomainException(ErrorCode.PaymentGatewayFailure).AddDetail("ErrorMessage", ex.Message);
}
}
}
}

Просмотреть файл

@ -0,0 +1,437 @@
// -----------------------------------------------------------------------
// <copyright file="PayUGateway.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce.PaymentGateways
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Threading.Tasks;
using Exceptions;
using Models;
using PartnerCenter.Models.Customers;
using PayUMoney;
/// <summary>
/// PayUMoney payment gateway implementation.
/// </summary>
public class PayUGateway : DomainObject, IPaymentGateway
{
/// <summary>
/// Maintains the description for this payment.
/// </summary>
private readonly string paymentDescription;
/// <summary>
/// Maintains the payer id for the payment gateway.
/// </summary>
private string payerId;
/// <summary>
/// Maintains the payment id for the payment gateway.
/// </summary>
private string paymentId;
/// <summary>
/// Initializes a new instance of the <see cref="PayUGateway" /> class.
/// </summary>
/// <param name="applicationDomain">The ApplicationDomain</param>
/// <param name="description">The description which will be added to the Payment Card authorization call.</param>
public PayUGateway(ApplicationDomain applicationDomain, string description) : base(applicationDomain)
{
description.AssertNotEmpty(nameof(description));
this.paymentDescription = description;
this.payerId = string.Empty;
this.paymentId = string.Empty;
}
/// <summary>
/// Validates payment configuration.
/// </summary>
/// <param name="paymentConfig">The Payment configuration.</param>
public void ValidateConfiguration(PaymentConfiguration paymentConfig)
{
////Payu does not provide payment profile validation api.
}
/// <summary>
/// Creates Web Experience profile using portal branding and payment configuration.
/// </summary>
/// <param name="paymentConfig">The Payment configuration.</param>
/// <param name="brandConfig">The branding configuration.</param>
/// <param name="countryIso2Code">The locale code used by the web experience profile. Example-US.</param>
/// <returns>The created web experience profile id.</returns>
public string CreateWebExperienceProfile(PaymentConfiguration paymentConfig, BrandingConfiguration brandConfig, string countryIso2Code)
{
////Payu does not provide the concept of webprofile
////stored authorization in WebExperienceProfileId
return paymentConfig.WebExperienceProfileId;
}
/// <summary>
/// Creates a payment transaction and returns the PayUMoney generated payment URL.
/// </summary>
/// <param name="returnUrl">The redirect url for PayUMoney callback to web store portal.</param>
/// <param name="order">The order details for which payment needs to be made.</param>
/// <returns>Payment URL from PayUMoney.</returns>
public async Task<string> GeneratePaymentUriAsync(string returnUrl, OrderViewModel order)
{
returnUrl.AssertNotEmpty(nameof(returnUrl));
order.AssertNotNull(nameof(order));
RemotePost myremotepost = await PrepareRemotePost(order, returnUrl);
return myremotepost.Post();
}
/// <summary>
/// Executes a PayU payment.
/// </summary>
/// <returns>Capture string id.</returns>
public async Task<string> ExecutePaymentAsync()
{
try
{
TransactionStatusResponse paymentResponse = await ApiCalls.GetPaymentStatus(this.paymentId);
if (paymentResponse != null && paymentResponse.Result.Count > 0 && paymentResponse.Result[0].Status.Equals(Constant.MoneyWithPayU, StringComparison.InvariantCultureIgnoreCase))
{
return paymentResponse.Result[0].Amount.ToString(CultureInfo.InvariantCulture);
}
}
catch (Exception ex)
{
this.ParsePayUException(ex);
}
return await Task.FromResult(string.Empty);
}
/// <summary>
/// Finalizes an authorized payment with PayU.
/// </summary>
/// <param name="authorizationCode">The authorization code for the payment to capture.</param>
/// <returns>A task.</returns>
public async Task CaptureAsync(string authorizationCode)
{
////PayU api not provided
await Task.FromResult(string.Empty);
}
/// <summary>
/// Voids an authorized payment with PayUMoney.
/// </summary>
/// <param name="authorizationCode">The authorization code for the payment to void.</param>
/// <returns>a Task</returns>
public async Task VoidAsync(string authorizationCode)
{
authorizationCode.AssertNotEmpty(nameof(authorizationCode));
// given the authorizationId string... Lookup the authorization to void it.
try
{
RefundResponse refundResponse = await ApiCalls.RefundPayment(this.payerId, authorizationCode);
if (refundResponse.Status != 0 || !refundResponse.Message.Equals("Refund Initiated", StringComparison.InvariantCulture))
{
throw new Exception("Error in refund");
}
}
catch (Exception ex)
{
this.ParsePayUException(ex);
}
}
/// <summary>
/// Retrieves the order details maintained for the payment gateway.
/// </summary>
/// <param name="payerId">The Payer Id.</param>
/// <param name="paymentId">The Payment Id.</param>
/// <param name="orderId">The Order Id.</param>
/// <param name="customerId">The Customer Id.</param>
/// <returns>The order associated with this payment transaction.</returns>
public async Task<OrderViewModel> GetOrderDetailsFromPaymentAsync(string payerId, string paymentId, string orderId, string customerId)
{
// this payment gateway ignores orderId & customerId.
payerId.AssertNotEmpty(nameof(payerId));
paymentId.AssertNotEmpty(nameof(paymentId));
this.payerId = payerId;
this.paymentId = paymentId;
return await this.GetOrderDetails();
}
/// <summary>
/// Generate hash.
/// </summary>
/// <param name="text">hash string.</param>
/// <returns>return string</returns>
private string GenerateHash512(string text)
{
byte[] message = Encoding.UTF8.GetBytes(text);
UnicodeEncoding ue = new UnicodeEncoding();
byte[] hashValue;
System.Security.Cryptography.SHA512Managed hashString = new System.Security.Cryptography.SHA512Managed();
string hex = string.Empty;
hashValue = hashString.ComputeHash(message);
foreach (byte x in hashValue)
{
hex += string.Format(CultureInfo.InvariantCulture, "{0:x2}", x);
}
return hex;
}
/// <summary>
/// Generate transaction id.
/// </summary>
/// <returns>return string</returns>
private string GenerateTransactionId()
{
Random rnd = new Random();
string strHash = this.GenerateHash512(rnd.ToString() + DateTime.Now);
string txnid1 = strHash.ToString(CultureInfo.InvariantCulture).Substring(0, 20);
return txnid1;
}
/// <summary>
/// Retrieves the order details maintained for the payment gateway.
/// </summary>
/// <returns>return order data.</returns>
private async Task<OrderViewModel> GetOrderDetails()
{
OrderViewModel orderFromPayment = null;
try
{
PaymentResponse paymentResponse = await ApiCalls.GetPaymentDetails(this.paymentId);
if (paymentResponse != null && paymentResponse.Result.Count > 0)
{
orderFromPayment = await this.GetOrderDetails(paymentResponse.Result[0].PostBackParam.Udf1, paymentResponse.Result[0].PostBackParam.ProductInformation, paymentResponse.Result[0].PostBackParam.Udf2);
}
}
catch (Exception ex)
{
this.ParsePayUException(ex);
}
return await Task.FromResult(orderFromPayment);
}
/// <summary>
/// get payment url.
/// </summary>
/// <param name="mode">mode of payment gateway.</param>
/// <returns>return string.</returns>
private string GetPaymentUrl(string mode)
{
////two modes are possible sandbox and live
if (mode.Equals("sandbox", StringComparison.InvariantCultureIgnoreCase))
{
return Constant.TESTPAYUURL;
}
return Constant.LIVEPAYUURL;
}
/// <summary>
/// prepares Remote post by populate all the necessary fields to generate PayUMoney post request.
/// </summary>
/// <param name="order">order details.</param>
/// <param name="returnUrl">return url.</param>
/// <returns>return remote post.</returns>
private async Task<RemotePost> PrepareRemotePost(OrderViewModel order, string returnUrl)
{
string fname = string.Empty;
string phone = string.Empty;
string email = string.Empty;
CustomerRegistrationRepository customerRegistrationRepository = new CustomerRegistrationRepository(ApplicationDomain.Instance);
CustomerViewModel customerRegistrationInfo = await customerRegistrationRepository.RetrieveAsync(order.CustomerId);
if (customerRegistrationInfo == null)
{
Customer customer = await ApplicationDomain.Instance.PartnerCenterClient.Customers.ById(order.CustomerId).GetAsync();
fname = customer.BillingProfile.DefaultAddress.FirstName;
phone = customer.BillingProfile.DefaultAddress.PhoneNumber;
email = customer.BillingProfile.Email;
}
else
{
fname = customerRegistrationInfo.FirstName;
phone = customerRegistrationInfo.Phone;
email = customerRegistrationInfo.Email;
}
decimal paymentTotal = 0;
StringBuilder productSubs = new StringBuilder();
StringBuilder prodQuants = new StringBuilder();
foreach (OrderSubscriptionItemViewModel subscriptionItem in order.Subscriptions)
{
productSubs.Append(":").Append(subscriptionItem.SubscriptionId);
prodQuants.Append(":").Append(subscriptionItem.Quantity.ToString(CultureInfo.InvariantCulture));
paymentTotal += Math.Round(subscriptionItem.Quantity * subscriptionItem.SeatPrice, Resources.Culture.NumberFormat.CurrencyDecimalDigits);
}
productSubs.Remove(0, 1);
prodQuants.Remove(0, 1);
System.Collections.Specialized.NameValueCollection inputs = new System.Collections.Specialized.NameValueCollection();
PaymentConfiguration payconfig = await this.GetAPaymentConfigAsync();
inputs.Add("key", payconfig.ClientId);
inputs.Add("txnid", GenerateTransactionId());
inputs.Add("amount", paymentTotal.ToString(CultureInfo.InvariantCulture));
inputs.Add("productinfo", productSubs.ToString());
inputs.Add("firstname", fname);
inputs.Add("phone", phone);
inputs.Add("email", email);
inputs.Add("udf1", order.OperationType.ToString());
inputs.Add("udf2", prodQuants.ToString());
inputs.Add("surl", returnUrl + "&payment=success&PayerId=" + inputs.Get("txnid"));
inputs.Add("furl", returnUrl + "&payment=failure&PayerId=" + inputs.Get("txnid"));
inputs.Add("service_provider", Constant.PAYUPAISASERVICEPROVIDER);
string hashString = inputs.Get("key") + "|" + inputs.Get("txnid") + "|" + inputs.Get("amount") + "|" + inputs.Get("productInfo") + "|" + inputs.Get("firstName") + "|" + inputs.Get("email") + "|" + inputs.Get("udf1") + "|" + inputs.Get("udf2") + "|||||||||" + payconfig.ClientSecret; // payconfig.ClientSecret;
string hash = this.GenerateHash512(hashString);
inputs.Add("hash", hash);
RemotePost myremotepost = new RemotePost();
myremotepost.SetUrl(this.GetPaymentUrl(payconfig.AccountType));
myremotepost.SetInputs(inputs);
return myremotepost;
}
/// <summary>
/// Throws PartnerDomainException by parsing PayUMoney exception.
/// </summary>
/// <param name="ex">Exceptions from PayUMoney API call.</param>
private void ParsePayUException(Exception ex)
{
throw new PartnerDomainException(ErrorCode.PaymentGatewayFailure).AddDetail("ErrorMessage", ex.Message);
}
/// <summary>
/// Retrieves the Order from a payment transaction.
/// </summary>
/// <param name="operation">operation data.</param>
/// <param name="prod">product data.</param>
/// <param name="quant">quantity data.</param>
/// <returns>The Order for which payment was made.</returns>
private async Task<OrderViewModel> GetOrderDetails(string operation, string prod, string quant)
{
OrderViewModel orderFromPayment = null;
try
{
orderFromPayment = new OrderViewModel();
List<OrderSubscriptionItemViewModel> orderSubscriptions = new List<OrderSubscriptionItemViewModel>();
orderFromPayment.OperationType = (CommerceOperationType)Enum.Parse(typeof(CommerceOperationType), operation, true);
string[] prodList = prod.Split(':');
string[] quantList = quant.Split(':');
for (int i = 0; i < prodList.Length; i++)
{
orderSubscriptions.Add(new OrderSubscriptionItemViewModel()
{
SubscriptionId = prodList[i],
OfferId = prodList[i],
Quantity = Convert.ToInt32(quantList[i], CultureInfo.InvariantCulture)
});
}
orderFromPayment.Subscriptions = orderSubscriptions;
}
catch (Exception ex)
{
this.ParsePayUException(ex);
}
return await Task.FromResult(orderFromPayment);
}
/// <summary>
/// Throws PartnerDomainException by parsing PayUMoney exception.
/// </summary>
/// <returns>return payment configuration</returns>
private async Task<PaymentConfiguration> GetAPaymentConfigAsync()
{
// Before getAPIContext ... set up PayUMoney configuration. This is an expensive call which can benefit from caching.
PaymentConfiguration paymentConfig = await ApplicationDomain.Instance.PaymentConfigurationRepository.RetrieveAsync();
return paymentConfig;
}
/// <summary>
/// Remote post class.
/// </summary>
private class RemotePost
{
/// <summary>
/// Maintains Url.
/// </summary>
private string url = string.Empty;
/// <summary>
/// Maintains Method.
/// </summary>
private readonly string method = "post";
/// <summary>
/// Maintains form name.
/// </summary>
private readonly string formName = "form1";
/// <summary>
/// Maintains input collection.
/// </summary>
private System.Collections.Specialized.NameValueCollection inputs = new System.Collections.Specialized.NameValueCollection();
/// <summary>
/// Retrieves the API Context for PayUMoney.
/// </summary>
/// <param name="u">url string.</param>
public void SetUrl(string u)
{
this.url = u;
}
/// <summary>
/// Retrieves the API Context for PayUMoney.
/// </summary>
/// <param name="name">name string.</param>
/// <param name="value">value string.</param>
public void Add(string name, string value)
{
this.inputs.Add(name, value);
}
/// <summary>
/// Retrieves the API Context for PayUMoney.
/// </summary>
/// <param name="inputs">collection of values.</param>
public void SetInputs(System.Collections.Specialized.NameValueCollection inputs)
{
this.inputs = inputs;
}
/// <summary>
/// prepare form string.
/// </summary>
/// <returns>return form string</returns>
public string Post()
{
System.Web.HttpContext.Current.Response.Clear();
StringBuilder responseForm = new StringBuilder();
responseForm.Append(string.Format(CultureInfo.InvariantCulture, "<form name=\"{0}\" method=\"{1}\" action=\"{2}\" >", this.formName, this.method, this.url));
for (int i = 0; i < this.inputs.Keys.Count; i++)
{
responseForm.Append(string.Format(CultureInfo.InvariantCulture, "<input name=\"{0}\" type=\"hidden\" value=\"{1}\">", this.inputs.Keys[i], this.inputs[this.inputs.Keys[i]]));
}
responseForm.Append("</form>");
responseForm.Append(string.Format(CultureInfo.InvariantCulture, "<script language='javascript'>document.{0}.submit();</script>", this.formName));
return responseForm.ToString();
}
}
}
}

Просмотреть файл

@ -0,0 +1,115 @@
// -----------------------------------------------------------------------
// <copyright file="ApiCalls.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce.PaymentGateways.PayUMoney
{
using System.Collections.Specialized;
using System.Globalization;
using System.Net.Http;
using System.Threading.Tasks;
using BusinessLogic;
using Models;
/// <summary>
/// class definition
/// </summary>
public static class ApiCalls
{
/// <summary>
/// Http client object to call web API
/// </summary>
private static HttpClient client = new HttpClient();
/// <summary>
/// Get payment response.
/// </summary>
/// <param name="paymentId">The PaymentId.</param>
/// <returns>returns PayUMoneyPaymentResponse.</returns>
public static async Task<PaymentResponse> GetPaymentDetails(string paymentId)
{
PaymentConfiguration payconfig = await GetPaymentConfigAsync().ConfigureAwait(false);
NameValueCollection header = new NameValueCollection
{
{ "Authorization", payconfig.WebExperienceProfileId }
};
PaymentResponse response = await PostAsync<PaymentResponse>(header, string.Format(CultureInfo.InvariantCulture, Constant.PaymentResponseUrl, payconfig.ClientId, paymentId)).ConfigureAwait(false);
return response;
}
/// <summary>
/// Get Payment status.
/// </summary>
/// <param name="paymentId">The PaymentId.</param>
/// <returns>returns transaction response.</returns>
public static async Task<TransactionStatusResponse> GetPaymentStatus(string paymentId)
{
PaymentConfiguration payconfig = await GetPaymentConfigAsync().ConfigureAwait(false);
NameValueCollection header = new NameValueCollection
{
{ "Authorization", payconfig.WebExperienceProfileId }
};
TransactionStatusResponse response = await PostAsync<TransactionStatusResponse>(header, string.Format(CultureInfo.InvariantCulture, Constant.PaymentStatusUrl, payconfig.ClientId, paymentId)).ConfigureAwait(false);
return response;
}
/// <summary>
/// Initiate Refund.
/// </summary>
/// <param name="paymentId">The PaymentId.</param>
/// <param name="amount">The Amount.</param>
/// <returns>returns PayUMoneyRefundResponse.</returns>
public static async Task<RefundResponse> RefundPayment(string paymentId, string amount)
{
PaymentConfiguration payconfig = await GetPaymentConfigAsync().ConfigureAwait(false);
NameValueCollection header = new NameValueCollection
{
{ "Authorization", payconfig.WebExperienceProfileId }
};
RefundResponse response = await PostAsync<RefundResponse>(header, string.Format(CultureInfo.InvariantCulture, Constant.PaymentRefundUrl, payconfig.ClientId, paymentId, amount)).ConfigureAwait(false);
return response;
}
/// <summary>
/// Get Payment configuration.
/// </summary>
/// <returns>return payment configuration</returns>
private static async Task<PaymentConfiguration> GetPaymentConfigAsync()
{
PaymentConfiguration paymentConfig = await ApplicationDomain.Instance.PaymentConfigurationRepository.RetrieveAsync().ConfigureAwait(false);
return paymentConfig;
}
/// <summary>
/// Make Post API call on give path with given header
/// </summary>
/// <typeparam name="T">Class Name</typeparam>
/// <param name="header">The Header</param>
/// <param name="path">Post URL path</param>
/// <returns>Returns response</returns>
private static async Task<T> PostAsync<T>(NameValueCollection header, string path)
{
HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post, path);
message.Headers.Add("Accept", "application/json");
message.Headers.TryAddWithoutValidation("Authorization", header.Get("Authorization"));
T data = default(T);
HttpResponseMessage response = await client.SendAsync(message).ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
data = await response.Content.ReadAsAsync<T>().ConfigureAwait(false);
}
return data;
}
}
}

Просмотреть файл

@ -0,0 +1,54 @@
// -----------------------------------------------------------------------
// <copyright file="Constant.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce.PaymentGateways.PayUMoney
{
/// <summary>
/// Constant class
/// </summary>
public static class Constant
{
/// <summary>
/// PaymentResponseUrl url.
/// </summary>
public const string PaymentResponseUrl = "https://www.payumoney.com/payment/op/getPaymentResponse?merchantKey={0}&merchantTransactionIds={1}";
/// <summary>
/// PaymentStatusUrl url.
/// </summary>
public const string PaymentStatusUrl = "https://www.payumoney.com/payment/payment/chkMerchantTxnStatus?merchantKey={0}&merchantTransactionIds={1}";
/// <summary>
/// PaymentRefundUrl url.
/// </summary>
public const string PaymentRefundUrl = "https://www.payumoney.com/treasury/merchant/refundPayment?merchantKey={0}&paymentId={1}&refundAmount={2}";
/// <summary>
/// MoneyWithPayU url.
/// </summary>
public const string MoneyWithPayU = "Money with Payumoney";
/// <summary>
/// Test url.
/// </summary>
public static readonly string TESTPAYUURL = "https://test.payu.in/_payment";
/// <summary>
/// Live url.
/// </summary>
public static readonly string LIVEPAYUURL = "https://secure.payu.in/_payment";
/// <summary>
/// Hash sequence.
/// </summary>
public static readonly string HASHSEQUENCE = "key|txnid|amount|productinfo|firstname|email|udf1|udf2|udf3|udf4|udf5|udf6|udf7|udf8|udf9|udf10";
/// <summary>
/// Maintains the payment id for the payment gateway.
/// </summary>
public static readonly string PAYUPAISASERVICEPROVIDER = "payu_paisa";
}
}

Просмотреть файл

@ -0,0 +1,41 @@
// -----------------------------------------------------------------------
// <copyright file="PaymentResponse.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce.PaymentGateways.PayUMoney
{
using System.Collections.Generic;
/// <summary>
/// PayUMoneyPaymentResponse class
/// </summary>
public class PaymentResponse
{
/// <summary>
/// Gets or sets error code
/// </summary>
public string ErrorCode { get; set; }
/// <summary>
/// Gets or sets message code
/// </summary>
public string Message { get; set; }
/// <summary>
/// Gets or sets response code
/// </summary>
public string ResponseCode { get; set; }
/// <summary>
/// Gets or sets result
/// </summary>
public List<PaymentResponseResult> Result { get; set; }
/// <summary>
/// Gets or sets status
/// </summary>
public string Status { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,24 @@
// -----------------------------------------------------------------------
// <copyright file="PaymentResponseResult.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce.PaymentGateways.PayUMoney
{
/// <summary>
/// result class
/// </summary>
public class PaymentResponseResult
{
/// <summary>
/// Gets or sets merchant transaction id
/// </summary>
public string MerchantTransactionId { get; set; }
/// <summary>
/// Gets or sets post back
/// </summary>
public PostBackParameters PostBackParam { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,442 @@
// -----------------------------------------------------------------------
// <copyright file="PostBackParameters.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce.PaymentGateways.PayUMoney
{
using System.Runtime.Serialization;
/// <summary>
/// PostBack Parameters class
/// </summary>
[DataContract]
public class PostBackParameters
{
/// <summary>
/// Gets or sets added on
/// </summary>
[DataMember(Name = "Addedon")]
public string AddedOn { get; set; }
/// <summary>
/// Gets or sets additional charges
/// </summary>
[DataMember(Name = "AdditionalCharges")]
public string AdditionalCharges { get; set; }
/// <summary>
/// Gets or sets additional parameter
/// </summary>
[DataMember(Name = "Additional_param")]
public string AdditionalParamameter { get; set; }
/// <summary>
/// Gets or sets address
/// </summary>
[DataMember(Name = "Address1")]
public string Address1 { get; set; }
/// <summary>
/// Gets or sets address2
/// </summary>
[DataMember(Name = "Address2")]
public string Address2 { get; set; }
/// <summary>
/// Gets or sets amount
/// </summary>
[DataMember(Name = "Amount")]
public string Amount { get; set; }
/// <summary>
/// Gets or sets amount split
/// </summary>
[DataMember(Name = "Amount_split")]
public string AmountSplit { get; set; }
/// <summary>
/// Gets or sets bank ref number
/// </summary>
[DataMember(Name = "Bank_ref_num")]
public string BankReferenceNumber { get; set; }
/// <summary>
/// Gets or sets bank code
/// </summary>
[DataMember(Name = "Bankcode")]
public string BankCode { get; set; }
/// <summary>
/// Gets or sets called status
/// </summary>
[DataMember(Name = "CalledStatus")]
public string CalledStatus { get; set; }
/// <summary>
/// Gets or sets card token
/// </summary>
[DataMember(Name = "CardToken")]
public string CardToken { get; set; }
/// <summary>
/// Gets or sets merchant parameter
/// </summary>
[DataMember(Name = "Card_merchant_param")]
public string CardMerchantParam { get; set; }
/// <summary>
/// Gets or sets card type
/// </summary>
[DataMember(Name = "CardType")]
public string Card_type { get; set; }
/// <summary>
/// Gets or sets card hash
/// </summary>
[DataMember(Name = "Cardhash")]
public string CardHash { get; set; }
/// <summary>
/// Gets or sets card number
/// </summary>
[DataMember(Name = "Cardnum")]
public string CardNumber { get; set; }
/// <summary>
/// Gets or sets city
/// </summary>
[DataMember(Name = "City")]
public string City { get; set; }
/// <summary>
/// Gets or sets country
/// </summary>
[DataMember(Name = "Country")]
public string Country { get; set; }
/// <summary>
/// Gets or sets created on
/// </summary>
[DataMember(Name = "CreatedOn")]
public string CreatedOn { get; set; }
/// <summary>
/// Gets or sets discount
/// </summary>
[DataMember(Name = "Discount")]
public string Discount { get; set; }
/// <summary>
/// Gets or sets email
/// </summary>
[DataMember(Name = "Email")]
public string Email { get; set; }
/// <summary>
/// Gets or sets encrypted PaymentId
/// </summary>
[DataMember(Name = "EncryptedPaymentId")]
public string EncryptedPaymentId { get; set; }
/// <summary>
/// Gets or sets error
/// </summary>
[DataMember(Name = "Error")]
public string Error { get; set; }
/// <summary>
/// Gets or sets error message
/// </summary>
[DataMember(Name = "Error_Message")]
public string ErrorMessage { get; set; }
/// <summary>
/// Gets or sets fetch
/// </summary>
[DataMember(Name = "FetchAPI")]
public string FetchAPI { get; set; }
/// <summary>
/// Gets or sets Field1
/// </summary>
[DataMember(Name = "Field1")]
public string Field1 { get; set; }
/// <summary>
/// Gets or sets Field2
/// </summary>
[DataMember(Name = "Field2")]
public string Field2 { get; set; }
/// <summary>
/// Gets or sets Field3
/// </summary>
[DataMember(Name = "Field3")]
public string Field3 { get; set; }
/// <summary>
/// Gets or sets Field4
/// </summary>
[DataMember(Name = "Field4")]
public string Field4 { get; set; }
/// <summary>
/// Gets or sets Field5
/// </summary>
[DataMember(Name = "Field5")]
public string Field5 { get; set; }
/// <summary>
/// Gets or sets Field6
/// </summary>
[DataMember(Name = "Field6")]
public string Field6 { get; set; }
/// <summary>
/// Gets or sets Field7
/// </summary>
[DataMember(Name = "Field7")]
public string Field7 { get; set; }
/// <summary>
/// Gets or sets Field8
/// </summary>
[DataMember(Name = "Field8")]
public string Field8 { get; set; }
/// <summary>
/// Gets or sets Field9
/// </summary>
[DataMember(Name = "Field9")]
public string Field9 { get; set; }
/// <summary>
/// Gets or sets first name
/// </summary>
[DataMember(Name = "Firstname")]
public string FirstName { get; set; }
/// <summary>
/// Gets or sets hash
/// </summary>
[DataMember(Name = "Hash")]
public string Hash { get; set; }
/// <summary>
/// Gets or sets key
/// </summary>
[DataMember(Name = "Key")]
public string Key { get; set; }
/// <summary>
/// Gets or sets last name
/// </summary>
[DataMember(Name = "Lastname")]
public string LastName { get; set; }
/// <summary>
/// Gets or sets me code
/// </summary>
[DataMember(Name = "MeCode")]
public string MeCode { get; set; }
/// <summary>
/// Gets or sets
/// </summary>
[DataMember(Name = "Mihpayid")]
public string MihPaymentId { get; set; }
/// <summary>
/// Gets or sets mode
/// </summary>
[DataMember(Name = "Mode")]
public string Mode { get; set; }
/// <summary>
/// Gets or sets name on card
/// </summary>
[DataMember(Name = "Name_on_card")]
public string NameOnCard { get; set; }
/// <summary>
/// Gets or sets net amount debit
/// </summary>
[DataMember(Name = "Net_amount_debit")]
public string NetAmountDebit { get; set; }
/// <summary>
/// Gets or sets offer availed
/// </summary>
[DataMember(Name = "Offer_availed")]
public string OfferAvailed { get; set; }
/// <summary>
/// Gets or sets offer failure reason
/// </summary>
[DataMember(Name = "Offer_failure_reason")]
public string OfferFailureReason { get; set; }
/// <summary>
/// Gets or sets offer key
/// </summary>
[DataMember(Name = "Offer_key")]
public string OfferKey { get; set; }
/// <summary>
/// Gets or sets offer type
/// </summary>
[DataMember(Name = "Offer_type")]
public string OfferType { get; set; }
/// <summary>
/// Gets or sets paisa me code
/// </summary>
[DataMember(Name = "Paisa_mecode")]
public string PaisaMeCode { get; set; }
/// <summary>
/// Gets or sets paymentId
/// </summary>
[DataMember(Name = "PaymentId")]
public string PaymentId { get; set; }
/// <summary>
/// Gets or sets payUMoneyId
/// </summary>
[DataMember(Name = "PayuMoneyId")]
public string PayuMoneyId { get; set; }
/// <summary>
/// Gets or sets Type
/// </summary>
[DataMember(Name = "Pg_TYPE")]
public string PgTYPE { get; set; }
/// <summary>
/// Gets or sets
/// </summary>
[DataMember(Name = "Pg_ref_no")]
public string PgReferenceNumber { get; set; }
/// <summary>
/// Gets or sets phone
/// </summary>
[DataMember(Name = "Phone")]
public string Phone { get; set; }
/// <summary>
/// Gets or sets
/// </summary>
[DataMember(Name = "PostBackParamId")]
public string PostBackParamameterId { get; set; }
/// <summary>
/// Gets or sets post url
/// </summary>
[DataMember(Name = "PostUrl")]
public string PostUrl { get; set; }
/// <summary>
/// Gets or sets product info
/// </summary>
[DataMember(Name = "Productinfo")]
public string ProductInformation { get; set; }
/// <summary>
/// Gets or sets state
/// </summary>
[DataMember(Name = "State")]
public string State { get; set; }
/// <summary>
/// Gets or sets status
/// </summary>
[DataMember(Name = "Status")]
public string Status { get; set; }
/// <summary>
/// Gets or sets transaction Id
/// </summary>
[DataMember(Name = "Txnid")]
public string TransactionId { get; set; }
/// <summary>
/// Gets or sets
/// </summary>
[DataMember(Name = "Udf1")]
public string Udf1 { get; set; }
/// <summary>
/// Gets or sets
/// </summary>
[DataMember(Name = "Udf10")]
public string Udf10 { get; set; }
/// <summary>
/// Gets or sets
/// </summary>
[DataMember(Name = "Udf2")]
public string Udf2 { get; set; }
/// <summary>
/// Gets or sets
/// </summary>
[DataMember(Name = "Udf3")]
public string Udf3 { get; set; }
/// <summary>
/// Gets or sets
/// </summary>
[DataMember(Name = "Udf4")]
public string Udf4 { get; set; }
/// <summary>
/// Gets or sets
/// </summary>
[DataMember(Name = "Udf5")]
public string Udf5 { get; set; }
/// <summary>
/// Gets or sets
/// </summary>
[DataMember(Name = "Udf6")]
public string Udf6 { get; set; }
/// <summary>
/// Gets or sets
/// </summary>
[DataMember(Name = "Udf7")]
public string Udf7 { get; set; }
/// <summary>
/// Gets or sets
/// </summary>
[DataMember(Name = "Udf8")]
public string Udf8 { get; set; }
/// <summary>
/// Gets or sets
/// </summary>
[DataMember(Name = "Udf9")]
public string Udf9 { get; set; }
/// <summary>
/// Gets or sets unmapped status
/// </summary>
[DataMember(Name = "Unmappedstatus")]
public string UnmappedStatus { get; set; }
/// <summary>
/// Gets or sets version
/// </summary>
[DataMember(Name = "Version")]
public string Version { get; set; }
/// <summary>
/// Gets or sets Zip code
/// </summary>
[DataMember(Name = "Zipcode")]
public string ZipCode { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,56 @@
// -----------------------------------------------------------------------
// <copyright file="RefundResponse.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce.PaymentGateways.PayUMoney
{
/// <summary>
/// Refund response class
/// </summary>
public class RefundResponse
{
/// <summary>
/// Gets or sets error code. Always Null from PayUMoney documentation.
/// </summary>
public string ErrorCode { get; set; }
/// <summary>
/// Gets or sets Guid.
/// </summary>
public string Guid { get; set; }
/// <summary>
/// Gets or sets message.
/// Message string for both success and failure cases
/// Refund Initiated : Refund successfully Initiated
/// PaymentId is not valid for this merchant : When PaymentID is not linked with the merchantID passed
/// Payment is not allowed for refund as status is: refunding progress : Refund on this sub order is already initiated
/// </summary>
public string Message { get; set; }
/// <summary>
/// Gets or sets result.
/// if Success then it has RefundId.
/// if Failure then it will be NULL.
/// </summary>
public string Result { get; set; }
/// <summary>
/// Gets or sets rows.
/// </summary>
public string Rows { get; set; }
/// <summary>
/// Gets or sets session ID.
/// </summary>
public string SessionId { get; set; }
/// <summary>
/// Gets or sets status.
/// Status will be 0 if API call is a success, Status will be -1 in case of failure you'll get system handled failure reasons in this case.
/// </summary>
public int Status { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,34 @@
// -----------------------------------------------------------------------
// <copyright file="TransactionResult.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce.PaymentGateways.PayUMoney
{
/// <summary>
/// Transaction result class.
/// </summary>
public class TransactionResult
{
/// <summary>
/// Gets or sets merchant transaction ID.
/// </summary>
public string MerchantTransactionId { get; set; }
/// <summary>
/// Gets or sets payment ID.
/// </summary>
public int PaymentId { get; set; }
/// <summary>
/// Gets or sets status.
/// </summary>
public string Status { get; set; }
/// <summary>
/// Gets or sets amount.
/// </summary>
public double Amount { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,42 @@
// -----------------------------------------------------------------------
// <copyright file="TransactionStatusResponse.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce.PaymentGateways.PayUMoney
{
using System.Collections.Generic;
/// <summary>
/// Transaction status response class.
/// </summary>
public class TransactionStatusResponse
{
/// <summary>
/// Gets or sets status.
/// Status will be 0 if API call is a success, Status will be -1 in case of failure you'll get system handled failure reasons in this case.
/// </summary>
public int Status { get; set; }
/// <summary>
/// Gets or sets message.
/// </summary>
public string Message { get; set; }
/// <summary>
/// Gets or sets transaction result.
/// </summary>
public List<TransactionResult> Result { get; set; }
/// <summary>
/// Gets or sets error code. Always Null from PayUMoney documentation.
/// </summary>
public object ErrorCode { get; set; }
/// <summary>
/// Gets or sets response code.
/// </summary>
public object ResponseCode { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,65 @@
// -----------------------------------------------------------------------
// <copyright file="PaymentGatewayConfig.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce.PaymentGateways
{
using System;
/// <summary>
/// Payment configuration class
/// </summary>
public static class PaymentGatewayConfig
{
/// <summary>
/// country code
/// </summary>
private static string countryCode = ApplicationDomain.Instance.PortalLocalization.CountryIso2Code;
/// <summary>
/// this method is use to get the payment configuration page
/// </summary>
/// <returns> return view name</returns>
public static string GetPaymentConfigView()
{
if (countryCode.Equals("in", StringComparison.InvariantCultureIgnoreCase))
{
return "PayUPaymentSetup";
}
return "PaymentSetup";
}
/// <summary>
/// Get web configuration path
/// </summary>
/// <returns>returns web configuration name</returns>
public static string GetWebConfigPath()
{
if (countryCode.Equals("in", StringComparison.InvariantCultureIgnoreCase))
{
return "WebPortalConfigurationPayU.json";
}
return "WebPortalConfiguration.json";
}
/// <summary>
/// creates a payment gateway instance
/// </summary>
/// <param name="applicationDomain">Application domain</param>
/// <param name="description">the description</param>
/// <returns>returns payment gateway instance</returns>
public static IPaymentGateway GetPaymentGatewayInstance(ApplicationDomain applicationDomain, string description)
{
if (countryCode.Equals("in", StringComparison.InvariantCultureIgnoreCase))
{
return new PayUGateway(applicationDomain, description);
}
return new PayPalGateway(applicationDomain, description);
}
}
}

Просмотреть файл

@ -0,0 +1,131 @@
// -----------------------------------------------------------------------
// <copyright file="PreApprovalGateway.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce.PaymentGateways
{
using System.Globalization;
using System.Threading.Tasks;
using Models;
/// <summary>
/// Payment gateway which allows for pre approved orders in the storefront.
/// </summary>
public class PreApprovalGateway : DomainObject, IPaymentGateway
{
/// <summary>
/// The order id for an individual order;
/// </summary>
private string orderId;
/// <summary>
/// The customer id for an individual order;
/// </summary>
private string customerId;
/// <summary>
/// Initializes a new instance of the <see cref="PreApprovalGateway" /> class.
/// </summary>
/// <param name="applicationDomain">The ApplicationDomain.</param>
/// <param name="description">The Payment description.</param>
public PreApprovalGateway(ApplicationDomain applicationDomain, string description) : base(applicationDomain)
{
}
/// <summary>
/// Stub to finalizes an authorized payment in the gateway.
/// </summary>
/// <param name="authorizationCode">The authorization code for the payment to capture.</param>
/// <returns>A task.</returns>
public async Task CaptureAsync(string authorizationCode)
{
// clean up the order item.
await ApplicationDomain.Instance.CustomerOrdersRepository.DeleteAsync(this.orderId, this.customerId);
}
/// <summary>
/// Stub to execute a payment.
/// </summary>
/// <returns>Capture string id.</returns>
public async Task<string> ExecutePaymentAsync()
{
return await Task.FromResult("Pre-approvedTransaction");
}
/// <summary>
/// Stub to generate payment url.
/// </summary>
/// <param name="returnUrl">App return url.</param>
/// <param name="order">Order information.</param>
/// <returns>Returns the process order page with success flags setup.</returns>
public async Task<string> GeneratePaymentUriAsync(string returnUrl, OrderViewModel order)
{
// will essentially return the returnUrl as is with additional decorations.
// persist the order.
OrderViewModel orderDetails = await ApplicationDomain.Instance.CustomerOrdersRepository.AddAsync(order);
// for future cleanup.
this.orderId = orderDetails.OrderId;
this.customerId = orderDetails.CustomerId;
string appReturnUrl = returnUrl + string.Format(CultureInfo.InvariantCulture, "&oid={0}&payment=success&PayerID=PayId&paymentId=PreApproved", orderDetails.OrderId);
return await Task.FromResult(appReturnUrl);
}
/// <summary>
/// Retrieves the order details maintained for the payment gateway.
/// </summary>
/// <param name="payerId">The Payer Id.</param>
/// <param name="paymentId">The Payment Id.</param>
/// <param name="orderId">The Order Id.</param>
/// <param name="customerId">The Customer Id.</param>
/// <returns>The order associated with this payment transaction.</returns>
public async Task<OrderViewModel> GetOrderDetailsFromPaymentAsync(string payerId, string paymentId, string orderId, string customerId)
{
// This gateway implementation ignores payerId, paymentId.
orderId.AssertNotEmpty(nameof(orderId));
customerId.AssertNotEmpty(nameof(customerId));
// for future cleanup.
this.orderId = orderId;
this.customerId = customerId;
// use order repository to extract details.
return await ApplicationDomain.Instance.CustomerOrdersRepository.RetrieveAsync(orderId, customerId);
}
/// <summary>
/// Stub to Void payment.
/// </summary>
/// <param name="authorizationCode">The authorization code for the payment to void.</param>
/// <returns>a Task</returns>
public async Task VoidAsync(string authorizationCode)
{
// clean up the order item.
await ApplicationDomain.Instance.CustomerOrdersRepository.DeleteAsync(this.orderId, this.customerId);
}
/// <summary>
/// Validates payment configuration.
/// </summary>
/// <param name="paymentConfig">The Payment configuration.</param>
public void ValidateConfiguration(PaymentConfiguration paymentConfig)
{
////No need to implement this method
}
/// <summary>
/// Creates Web Experience profile using portal branding and payment configuration.
/// </summary>
/// <param name="paymentConfig">The Payment configuration.</param>
/// <param name="brandConfig">The branding configuration.</param>
/// <param name="countryIso2Code">The locale code used by the web experience profile. Example-US.</param>
/// <returns>The created web experience profile id.</returns>
public string CreateWebExperienceProfile(PaymentConfiguration paymentConfig, BrandingConfiguration brandConfig, string countryIso2Code)
{
////no need to implement this method
return string.Empty;
}
}
}

Просмотреть файл

@ -0,0 +1,217 @@
// -----------------------------------------------------------------------
// <copyright file="PreApprovedCustomersRepository.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Models;
using Newtonsoft.Json;
using PartnerCenter.Models.Customers;
using PartnerCenter.Models.Query;
using WindowsAzure.Storage.Blob;
/// <summary>
/// Manages persistence for PreApprovedCustomersRepository configuration options.
/// </summary>
public class PreApprovedCustomersRepository : DomainObject
{
/// <summary>
/// The PreApprovedCustomers key in the cache.
/// </summary>
private const string PreApprovedCustomersCacheKey = "PreApprovedCustomers";
/// <summary>
/// The Azure BLOB name for the portal PreApprovedCustomers configuration.
/// </summary>
private const string PreApprovedCustomersBlobName = "preapprovedcustomers";
/// <summary>
/// Initializes a new instance of the <see cref="PreApprovedCustomersRepository"/> class.
/// </summary>
/// <param name="applicationDomain">An application domain instance.</param>
public PreApprovedCustomersRepository(ApplicationDomain applicationDomain) : base(applicationDomain)
{
}
/// <summary>
/// Retrieves the PreApproved Customers for rendering in UX.
/// </summary>
/// <returns>The PreApproved Customers.</returns>
public async Task<PreApprovedCustomersViewModel> RetrieveCustomerDetailsAsync()
{
// retrieve the list of customers from Partner Center.
var sdkClient = ApplicationDomain.Instance.PartnerCenterClient;
List<Customer> allCustomers = new List<Customer>();
// create a customer enumerator which will aid us in traversing the customer pages
var customersEnumerator = sdkClient.Enumerators.Customers.Create(sdkClient.Customers.Query(QueryFactory.Instance.BuildIndexedQuery(100)));
while (customersEnumerator.HasValue)
{
foreach (Customer c in customersEnumerator.Current.Items)
{
allCustomers.Add(c);
}
customersEnumerator.Next();
}
// if all customers are preapproved then every customer's IsPreApproved is true.
bool allCustomersPreApproved = false;
PreApprovedCustomersList currentPreApprovedCustomers = await this.RetrieveAsync();
if (currentPreApprovedCustomers.CustomerIds != null)
{
// Find if the all customers approved entry is present.
allCustomersPreApproved = currentPreApprovedCustomers.CustomerIds.Where(cid => (cid == Guid.Empty.ToString())).Count() > 0;
}
// populate portal customer list.
List<PortalCustomer> preApprovedCustomerDetails = (from customer in allCustomers
select new PortalCustomer()
{
TenantId = customer.Id,
CompanyName = customer.CompanyProfile.CompanyName,
Domain = customer.CompanyProfile.Domain,
IsPreApproved = false
}).ToList();
// identify the customers who are preapproved and update them.
if (!allCustomersPreApproved && (currentPreApprovedCustomers.CustomerIds != null))
{
foreach (string customerId in currentPreApprovedCustomers.CustomerIds)
{
try
{
// can raise an exception if a customer has been removed from PartnerCenter although preapproved in the portal.
preApprovedCustomerDetails.Where(customer => customer.TenantId == customerId).FirstOrDefault().IsPreApproved = true;
}
catch (NullReferenceException)
{
}
}
}
return new PreApprovedCustomersViewModel()
{
IsEveryCustomerPreApproved = allCustomersPreApproved,
CustomerIds = allCustomersPreApproved ? null : currentPreApprovedCustomers.CustomerIds?.ToList(),
Items = preApprovedCustomerDetails.OrderBy(customer => customer.CompanyName)
};
}
/// <summary>
/// Updates the PreApproved Customers configuration.
/// </summary>
/// <param name="preApprovedCustomers">The new list of PreApproved Customers.</param>
/// <returns>The updated PreApprovedCustomers configuration.</returns>
public async Task<PreApprovedCustomersViewModel> UpdateAsync(PreApprovedCustomersViewModel preApprovedCustomers)
{
preApprovedCustomers.AssertNotNull(nameof(preApprovedCustomers));
PreApprovedCustomersList customerList = new PreApprovedCustomersList();
if (preApprovedCustomers.IsEveryCustomerPreApproved)
{
string[] ids = new string[] { Guid.Empty.ToString() };
customerList.CustomerIds = ids.ToList();
}
else
{
customerList.CustomerIds = preApprovedCustomers.CustomerIds;
}
var preApprovedCustomersBlob = await this.GetPreApprovedCustomersBlob();
await preApprovedCustomersBlob.UploadTextAsync(JsonConvert.SerializeObject(customerList));
// invalidate the cache, we do not update it to avoid race condition between web instances
await this.ApplicationDomain.CachingService.ClearAsync(PreApprovedCustomersRepository.PreApprovedCustomersCacheKey);
return await this.RetrieveCustomerDetailsAsync();
}
/// <summary>
/// Checks whether given customer id is of a pre approved customer or not.
/// </summary>
/// <param name="customerId">The customer who is transacting.</param>
/// <returns>True if customer is pre approved else false.</returns>
public async Task<bool> IsCustomerPreApprovedAsync(string customerId)
{
customerId.AssertNotEmpty(nameof(customerId));
bool isCustomerPreApproved = false;
PreApprovedCustomersList existingCustomers = await this.RetrieveAsync();
if (existingCustomers.CustomerIds != null)
{
// Find if the all customers approved entry is present.
int allCustomersApproved = existingCustomers.CustomerIds.Where(cid => (cid == Guid.Empty.ToString())).Count();
if (allCustomersApproved > 0)
{
isCustomerPreApproved = true;
}
else
{
// check if current customer is in the pre approved list.
int currentCustomerApproved = existingCustomers.CustomerIds.Where(cid => cid == customerId).Count();
isCustomerPreApproved = currentCustomerApproved > 0;
}
}
return isCustomerPreApproved;
}
/// <summary>
/// Retrieves the portal PreApproved Customers configuration BLOB reference.
/// </summary>
/// <returns>The portal PreApproved Customers BLOB.</returns>
private async Task<CloudBlockBlob> GetPreApprovedCustomersBlob()
{
var portalAssetsBlobContainer = await this.ApplicationDomain.AzureStorageService.GetPrivateCustomerPortalAssetsBlobContainerAsync();
return portalAssetsBlobContainer.GetBlockBlobReference(PreApprovedCustomersRepository.PreApprovedCustomersBlobName);
}
/// <summary>
/// Retrieves the PreApproved Customers from persistence.
/// </summary>
/// <returns>The PreApproved Customers.</returns>
private async Task<PreApprovedCustomersList> RetrieveAsync()
{
var preApprovedCustomersList = await this.ApplicationDomain.CachingService
.FetchAsync<PreApprovedCustomersList>(PreApprovedCustomersRepository.PreApprovedCustomersCacheKey);
if (preApprovedCustomersList == null)
{
var preApprovedCustomersBlob = await this.GetPreApprovedCustomersBlob();
preApprovedCustomersList = new PreApprovedCustomersList();
if (await preApprovedCustomersBlob.ExistsAsync())
{
preApprovedCustomersList = JsonConvert.DeserializeObject<PreApprovedCustomersList>(await preApprovedCustomersBlob.DownloadTextAsync());
// cache the preapproved customers configuration
await this.ApplicationDomain.CachingService.StoreAsync<PreApprovedCustomersList>(
PreApprovedCustomersRepository.PreApprovedCustomersCacheKey,
preApprovedCustomersList);
}
}
return preApprovedCustomersList;
}
/// <summary>
/// The Pre Approved customers list model.
/// </summary>
private class PreApprovedCustomersList
{
/// <summary>
/// Gets or sets the customer ids who are preapproved.
/// </summary>
public IEnumerable<string> CustomerIds { get; set; }
}
}
}

Просмотреть файл

@ -0,0 +1,79 @@
// -----------------------------------------------------------------------
// <copyright file="AuthorizePayment.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce.Transactions
{
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Infrastructure;
/// <summary>
/// Authorizes a payment with a payment gateway.
/// </summary>
public class AuthorizePayment : IBusinessTransactionWithOutput<string>
{
/// <summary>
/// Initializes a new instance of the <see cref="AuthorizePayment"/> class.
/// </summary>
/// <param name="paymentGateway">The payment gateway to use for authorization.</param>
public AuthorizePayment(IPaymentGateway paymentGateway)
{
paymentGateway.AssertNotNull(nameof(paymentGateway));
this.PaymentGateway = paymentGateway;
}
/// <summary>
/// Gets the payment gateway used for authorization.
/// </summary>
public IPaymentGateway PaymentGateway { get; private set; }
/// <summary>
/// Gets the authorization code.
/// </summary>
public string Result { get; private set; }
/// <summary>
/// Authorizes the payment amount.
/// </summary>
/// <returns>A task.</returns>
public async Task ExecuteAsync()
{
// authorize with the payment gateway
this.Result = await this.PaymentGateway.ExecutePaymentAsync();
}
/// <summary>
/// Rolls back the authorization.
/// </summary>
/// <returns>A task.</returns>
public async Task RollbackAsync()
{
if (!string.IsNullOrWhiteSpace(this.Result))
{
try
{
// void the previously authorized payment
await this.PaymentGateway.VoidAsync(this.Result);
}
catch (Exception voidingProblem)
{
if (voidingProblem.IsFatal())
{
throw;
}
Trace.TraceError("AuthorizePayment.RollbackAsync failed: {0}. Authorization code: {1}", voidingProblem, this.Result);
// TODO: Notify the system integrity recovery component
}
this.Result = string.Empty;
}
}
}
}

Просмотреть файл

@ -0,0 +1,89 @@
// -----------------------------------------------------------------------
// <copyright file="CapturePayment.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce.Transactions
{
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Infrastructure;
/// <summary>
/// Captures a payment.
/// </summary>
public class CapturePayment : IBusinessTransactionWithInput<string>
{
/// <summary>
/// Initializes a new instance of the <see cref="CapturePayment"/> class.
/// </summary>
/// <param name="paymentGateway">The payment gateway to use for capturing payments.</param>
/// <param name="authorizationCode">The authorization code to capture.</param>
public CapturePayment(IPaymentGateway paymentGateway, string authorizationCode)
{
paymentGateway.AssertNotNull(nameof(paymentGateway));
authorizationCode.AssertNotEmpty(nameof(paymentGateway));
this.PaymentGateway = paymentGateway;
this.AuthorizationCode = authorizationCode;
}
/// <summary>
/// Initializes a new instance of the <see cref="CapturePayment"/> class.
/// </summary>
/// <param name="paymentGateway">The payment gateway to use for capturing payments.</param>
/// <param name="acquireAuthorizationCallFunction">The function to call to obtain the authorization code.</param>
public CapturePayment(IPaymentGateway paymentGateway, Func<string> acquireAuthorizationCallFunction)
{
paymentGateway.AssertNotNull(nameof(paymentGateway));
acquireAuthorizationCallFunction.AssertNotNull(nameof(acquireAuthorizationCallFunction));
this.PaymentGateway = paymentGateway;
this.AcquireInput = acquireAuthorizationCallFunction;
}
/// <summary>
/// Gets the function that is called to retrieve the authorization code.
/// </summary>
public Func<string> AcquireInput { get; private set; }
/// <summary>
/// Gets the authorization code used for capturing payments.
/// </summary>
public string AuthorizationCode { get; private set; }
/// <summary>
/// Gets the payment gateway used to capture payments.
/// </summary>
public IPaymentGateway PaymentGateway { get; private set; }
/// <summary>
/// Captures the payment.
/// </summary>
/// <returns>A task.</returns>
public async Task ExecuteAsync()
{
if (string.IsNullOrEmpty(this.AuthorizationCode))
{
this.AuthorizationCode = this.AcquireInput.Invoke();
}
await this.PaymentGateway.CaptureAsync(this.AuthorizationCode);
}
/// <summary>
/// Rolls back the payment capture.
/// </summary>
/// <returns>A task.</returns>
public async Task RollbackAsync()
{
// no known way to rollback a captured payment, just log the fact
Trace.TraceInformation("CapturePayment.RollbackAsync executed. Authorization code: {0}", this.AuthorizationCode);
// TODO: Notify the system integrity recovery component
await Task.FromResult(0);
}
}
}

Просмотреть файл

@ -0,0 +1,140 @@
// -----------------------------------------------------------------------
// <copyright file="PersistNewlyPurchasedSubscriptions.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce.Transactions
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Infrastructure;
using Models;
using PartnerCenter.Models.Orders;
/// <summary>
/// A transaction which records all the resulting subscriptions of a Partner Center order into persistence. The customer subscriptions
/// and purchases tables will store the new subscriptions and their purchase history.
/// </summary>
public class PersistNewlyPurchasedSubscriptions :
IBusinessTransactionWithInput<Tuple<Order, IEnumerable<PurchaseLineItemWithOffer>>>,
IBusinessTransactionWithOutput<IEnumerable<TransactionResultLineItem>>
{
/// <summary>
/// An aggregate transaction which add all the subscriptions from an order to persistence.
/// </summary>
private IBusinessTransaction bulkSubscriptionPersistenceTransaction = null;
/// <summary>
/// Initializes a new instance of the <see cref="PersistNewlyPurchasedSubscriptions"/> class.
/// </summary>
/// <param name="customerId">The ID of the customer who performed the purchases.</param>
/// <param name="subscriptionsRepository">The customer subscriptions repository used to persist the subscriptions.</param>
/// <param name="purchasesRepository">The customer purchases repository used to persist the purchases.</param>
/// <param name="acquireInputsFunction">The function used to obtain the order and the list of purchase line items associated with their partner offers.</param>
public PersistNewlyPurchasedSubscriptions(
string customerId,
CustomerSubscriptionsRepository subscriptionsRepository,
CustomerPurchasesRepository purchasesRepository,
Func<Tuple<Order, IEnumerable<PurchaseLineItemWithOffer>>> acquireInputsFunction)
{
customerId.AssertNotEmpty(nameof(customerId));
subscriptionsRepository.AssertNotNull(nameof(subscriptionsRepository));
purchasesRepository.AssertNotNull(nameof(purchasesRepository));
acquireInputsFunction.AssertNotNull(nameof(acquireInputsFunction));
this.CustomerId = customerId;
this.CustomerSubscriptionsRepository = subscriptionsRepository;
this.CustomerPurchasesRepository = purchasesRepository;
this.AcquireInput = acquireInputsFunction;
}
/// <summary>
/// Gets the ID of the customer who owns the transaction.
/// </summary>
public string CustomerId { get; private set; }
/// <summary>
/// Gets the customer subscriptions repository used to persist the subscriptions.
/// </summary>
public CustomerSubscriptionsRepository CustomerSubscriptionsRepository { get; private set; }
/// <summary>
/// Gets the customer purchases repository used to persist the purchases.
/// </summary>
public CustomerPurchasesRepository CustomerPurchasesRepository { get; private set; }
/// <summary>
/// Gets the function used to obtain the order and the list of purchase line items associated with their partner offers.
/// </summary>
public Func<Tuple<Order, IEnumerable<PurchaseLineItemWithOffer>>> AcquireInput { get; private set; }
/// <summary>
/// Gets the result from running this transaction.
/// </summary>
public IEnumerable<TransactionResultLineItem> Result { get; private set; }
/// <summary>
/// Records all the resulting subscriptions as well as their initial purchase history into persistence.
/// </summary>
/// <returns>A task.</returns>
public async Task ExecuteAsync()
{
var inputs = this.AcquireInput.Invoke();
Order partnerCenterPurchaseOrder = inputs.Item1;
IEnumerable<PurchaseLineItemWithOffer> purchaseLineItems = inputs.Item2;
ICollection<TransactionResultLineItem> transactionResultLineItems = new List<TransactionResultLineItem>();
ICollection<IBusinessTransaction> persistenceTransactions = new List<IBusinessTransaction>();
DateTime rightNow = DateTime.UtcNow;
foreach (var orderLineItem in partnerCenterPurchaseOrder.LineItems)
{
var matchingPartnerOffer = purchaseLineItems.ElementAt(orderLineItem.LineItemNumber).PartnerOffer;
// add a record new customer subscription transaction for the current line item
persistenceTransactions.Add(new RecordNewCustomerSubscription(
this.CustomerSubscriptionsRepository,
new CustomerSubscriptionEntity(this.CustomerId, orderLineItem.SubscriptionId, matchingPartnerOffer.Id, rightNow.AddYears(1))));
// add a record purchase history for the current line item
persistenceTransactions.Add(new RecordPurchase(
this.CustomerPurchasesRepository,
new CustomerPurchaseEntity(CommerceOperationType.NewPurchase, Guid.NewGuid().ToString(), this.CustomerId, orderLineItem.SubscriptionId, orderLineItem.Quantity, matchingPartnerOffer.Price, rightNow)));
// build the transaction result line item
transactionResultLineItems.Add(new TransactionResultLineItem(
orderLineItem.SubscriptionId,
matchingPartnerOffer.Id,
orderLineItem.Quantity,
matchingPartnerOffer.Price,
matchingPartnerOffer.Price * orderLineItem.Quantity));
}
// bundle up all the transactions together
this.bulkSubscriptionPersistenceTransaction = new SequentialAggregateTransaction(persistenceTransactions);
// execute it!
await this.bulkSubscriptionPersistenceTransaction.ExecuteAsync();
// store the reuslting transaction line items
this.Result = transactionResultLineItems;
}
/// <summary>
/// Rollback all the inserts.
/// </summary>
/// <returns>A task.</returns>
public async Task RollbackAsync()
{
if (this.bulkSubscriptionPersistenceTransaction != null)
{
await this.bulkSubscriptionPersistenceTransaction.RollbackAsync();
this.bulkSubscriptionPersistenceTransaction = null;
}
}
}
}

Просмотреть файл

@ -0,0 +1,135 @@
// -----------------------------------------------------------------------
// <copyright file="PlaceOrder.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce.Transactions
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Exceptions;
using Infrastructure;
using PartnerCenter.Customers;
using PartnerCenter.Exceptions;
using PartnerCenter.Models.Orders;
using PartnerCenter.Models.Subscriptions;
/// <summary>
/// A transaction that places an order with partner center and knows how to roll it back.
/// </summary>
public class PlaceOrder : IBusinessTransactionWithOutput<Order>
{
/// <summary>
/// Initializes a new instance of the <see cref="PlaceOrder"/> class.
/// </summary>
/// <param name="customerOperations">A customer operations used to place the order.</param>
/// <param name="orderToPlace">A preconfigured order to place with Partner Center.</param>
public PlaceOrder(ICustomer customerOperations, Order orderToPlace)
{
customerOperations.AssertNotNull(nameof(customerOperations));
orderToPlace.AssertNotNull(nameof(orderToPlace));
this.Customer = customerOperations;
this.Order = orderToPlace;
}
/// <summary>
/// Gets the customer operations used to place the order.
/// </summary>
public ICustomer Customer { get; private set; }
/// <summary>
/// Gets the order to place.
/// </summary>
public Order Order { get; private set; }
/// <summary>
/// Gets the resulting order from the transaction.
/// </summary>
public Order Result { get; private set; }
/// <summary>
/// Places the order with Partner Center.
/// </summary>
/// <returns>A task.</returns>
public async Task ExecuteAsync()
{
try
{
// place the order
this.Result = await this.Customer.Orders.CreateAsync(this.Order);
}
catch (PartnerException orderPlacingProblem)
{
switch (orderPlacingProblem.ErrorCategory)
{
case PartnerErrorCategory.BadInput:
throw new PartnerDomainException(ErrorCode.InvalidInput, "PlaceOrder.ExecuteAsync() Failed", orderPlacingProblem);
case PartnerErrorCategory.AlreadyExists:
throw new PartnerDomainException(ErrorCode.AlreadyExists, "PlaceOrder.ExecuteAsync() Failed", orderPlacingProblem);
default:
throw new PartnerDomainException(ErrorCode.DownstreamServiceError, "PlaceOrder.ExecuteAsync() Failed", orderPlacingProblem);
}
}
}
/// <summary>
/// Rolls back the order that was placed.
/// </summary>
/// <returns>A task.</returns>
public async Task RollbackAsync()
{
if (this.Result != null)
{
// suspend all subscriptions that resulted from placing the order
IEnumerable<Task> suspensionTasks = this.Result.LineItems.Select<OrderLineItem, Task>(orderLineItem => new TaskFactory().StartNew(async () =>
{
try
{
Subscriptions.ISubscription subscriptionOperations = this.Customer.Subscriptions.ById(orderLineItem.SubscriptionId);
Subscription subscriptionToSuspend = await subscriptionOperations.GetAsync().ConfigureAwait(false);
subscriptionToSuspend.Status = SubscriptionStatus.Suspended;
subscriptionToSuspend.FriendlyName = subscriptionToSuspend.FriendlyName.Replace(Resources.UnpaidSubscriptionSuffix, string.Empty) + Resources.UnpaidSubscriptionSuffix;
Subscription patchedSubscription = await subscriptionOperations.PatchAsync(subscriptionToSuspend);
Trace.TraceInformation("Suspended subscription: {0}", orderLineItem.SubscriptionId);
}
catch (Exception suspensionProblem)
{
if (suspensionProblem.IsFatal())
{
throw;
}
Trace.TraceError("PlaceOrder.RollbackAsync: failed to suspend a subscription: {0}, ID: {1}", suspensionProblem, orderLineItem.SubscriptionId);
// TODO: Notify the system integrity recovery component
}
}, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.FromCurrentSynchronizationContext()));
try
{
await Task.WhenAll(suspensionTasks);
}
catch (Exception exception)
{
if (exception.IsFatal())
{
throw;
}
Trace.TraceError("PlaceOrder.RollbackAsync: awaiting all suspension tasks failed: {0}", exception);
}
this.Result = null;
}
}
}
}

Просмотреть файл

@ -0,0 +1,115 @@
// -----------------------------------------------------------------------
// <copyright file="PurchaseExtraSeats.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce.Transactions
{
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Exceptions;
using Infrastructure;
using PartnerCenter.Exceptions;
using PartnerCenter.Models.Subscriptions;
using Subscriptions;
/// <summary>
/// Purchases additional seats for a subscription.
/// </summary>
public class PurchaseExtraSeats : IBusinessTransactionWithOutput<Subscription>
{
/// <summary>
/// The subscription's seat count before the update.
/// </summary>
private int originalSeatCount;
/// <summary>
/// Initializes a new instance of the <see cref="PurchaseExtraSeats"/> class.
/// </summary>
/// <param name="subscriptionOperations">A Partner Center subscription operations instance.</param>
/// <param name="seatsToPurchase">The number of seats to purchase.</param>
public PurchaseExtraSeats(ISubscription subscriptionOperations, int seatsToPurchase)
{
subscriptionOperations.AssertNotNull(nameof(subscriptionOperations));
seatsToPurchase.AssertPositive(nameof(seatsToPurchase));
this.SubscriptionOperations = subscriptionOperations;
this.SeatsToPurchase = seatsToPurchase;
}
/// <summary>
/// Gets the subscription operations used to manipulate the subscription.
/// </summary>
public ISubscription SubscriptionOperations { get; private set; }
/// <summary>
/// Gets the number of seats to purchase.
/// </summary>
public int SeatsToPurchase { get; private set; }
/// <summary>
/// Gets the updated subscription.
/// </summary>
public Subscription Result { get; private set; }
/// <summary>
/// Purchases additional seats for the subscription.
/// </summary>
/// <returns>A task.</returns>
public async Task ExecuteAsync()
{
try
{
var partnerCenterSubscription = await this.SubscriptionOperations.GetAsync();
this.originalSeatCount = partnerCenterSubscription.Quantity;
partnerCenterSubscription.Quantity += this.SeatsToPurchase;
this.Result = await this.SubscriptionOperations.PatchAsync(partnerCenterSubscription);
}
catch (PartnerException subscriptionUpdateProblem)
{
if (subscriptionUpdateProblem.ErrorCategory == PartnerErrorCategory.NotFound)
{
throw new PartnerDomainException(ErrorCode.SubscriptionNotFound, "PurchaseExtraSeats.ExecuteAsync() Failed", subscriptionUpdateProblem);
}
else
{
throw new PartnerDomainException(ErrorCode.SubscriptionUpdateFailure, "PurchaseExtraSeats.ExecuteAsync() Failed", subscriptionUpdateProblem);
}
}
}
/// <summary>
/// Reverts the seat change.
/// </summary>
/// <returns>A task.</returns>
public async Task RollbackAsync()
{
if (this.Result != null)
{
try
{
// restore the original seat count for the subscription
this.Result.Quantity = this.originalSeatCount;
await this.SubscriptionOperations.PatchAsync(this.Result);
}
catch (Exception subscriptionUpdateProblem)
{
if (subscriptionUpdateProblem.IsFatal())
{
throw;
}
Trace.TraceError("PurchaseExtraSeats.RollbackAsync failed: {0}, ID: {1}, Quantity: {2}.", subscriptionUpdateProblem, this.Result.Id, this.Result.Quantity);
// TODO: Notify the system integrity recovery component
}
}
this.Result = null;
}
}
}

Просмотреть файл

@ -0,0 +1,91 @@
// -----------------------------------------------------------------------
// <copyright file="RecordNewCustomerSubscription.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce.Transactions
{
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Infrastructure;
using Models;
/// <summary>
/// Records a new subscription which a custom purchased.
/// </summary>
public class RecordNewCustomerSubscription : IBusinessTransactionWithOutput<CustomerSubscriptionEntity>
{
/// <summary>
/// Initializes a new instance of the <see cref="RecordNewCustomerSubscription"/> class.
/// </summary>
/// <param name="repository">A customer subscriptions repository which manages customer subscriptions persistence.</param>
/// <param name="newSubscription">The new customer subscription to record.</param>
public RecordNewCustomerSubscription(CustomerSubscriptionsRepository repository, CustomerSubscriptionEntity newSubscription)
{
repository.AssertNotNull(nameof(repository));
newSubscription.AssertNotNull(nameof(newSubscription));
this.CustomerSubscriptionsRepository = repository;
this.CustomerSubscriptionToPersist = newSubscription;
}
/// <summary>
/// Gets the customer subscription repository used to persist the subscription.
/// </summary>
public CustomerSubscriptionsRepository CustomerSubscriptionsRepository { get; private set; }
/// <summary>
/// Gets the customer subscription entity to persist.
/// </summary>
public CustomerSubscriptionEntity CustomerSubscriptionToPersist { get; private set; }
/// <summary>
/// Gets the resulting customer subscription entity.
/// </summary>
public CustomerSubscriptionEntity Result { get; private set; }
/// <summary>
/// Persists the subscription.
/// </summary>
/// <returns>A task.</returns>
public async Task ExecuteAsync()
{
this.Result = await this.CustomerSubscriptionsRepository.AddAsync(this.CustomerSubscriptionToPersist);
}
/// <summary>
/// Removes the subscription from persistence.
/// </summary>
/// <returns>A task.</returns>
public async Task RollbackAsync()
{
if (this.Result != null)
{
try
{
// delete the inserted row
await this.CustomerSubscriptionsRepository.DeleteAsync(this.Result);
}
catch (Exception deletionProblem)
{
if (deletionProblem.IsFatal())
{
throw;
}
Trace.TraceError(
"RecordNewCustomerSubscription.RollbackAsync failed: {0}, Customer ID: {1}, Subscription ID: {2}",
deletionProblem,
this.Result.CustomerId,
this.Result.SubscriptionId);
// TODO: Notify the system integrity recovery component
}
this.Result = null;
}
}
}
}

Просмотреть файл

@ -0,0 +1,92 @@
// -----------------------------------------------------------------------
// <copyright file="RecordPurchase.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce.Transactions
{
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Infrastructure;
using Models;
/// <summary>
/// Records a purchase the customer just made. A purchase can result in a new
/// subscription or extending an existing one or adding additional seats.
/// </summary>
public class RecordPurchase : IBusinessTransactionWithOutput<CustomerPurchaseEntity>
{
/// <summary>
/// Initializes a new instance of the <see cref="RecordPurchase"/> class.
/// </summary>
/// <param name="repository">A customer purchases repository which manages customer purchases persistence.</param>
/// <param name="newPurchaseRecord">The new customer purchase to record.</param>
public RecordPurchase(CustomerPurchasesRepository repository, CustomerPurchaseEntity newPurchaseRecord)
{
repository.AssertNotNull(nameof(repository));
newPurchaseRecord.AssertNotNull(nameof(newPurchaseRecord));
this.CustomerPurchasesRepository = repository;
this.CustomerPurchaseToPersist = newPurchaseRecord;
}
/// <summary>
/// Gets the customer purchase repository used to persist the purchase.
/// </summary>
public CustomerPurchasesRepository CustomerPurchasesRepository { get; private set; }
/// <summary>
/// Gets the customer purchase entity to persist.
/// </summary>
public CustomerPurchaseEntity CustomerPurchaseToPersist { get; private set; }
/// <summary>
/// Gets the resulting customer purchase entity.
/// </summary>
public CustomerPurchaseEntity Result { get; private set; }
/// <summary>
/// Persists the purchase.
/// </summary>
/// <returns>A task.</returns>
public async Task ExecuteAsync()
{
this.Result = await this.CustomerPurchasesRepository.AddAsync(this.CustomerPurchaseToPersist);
}
/// <summary>
/// Removes the purchase from persistence.
/// </summary>
/// <returns>A task.</returns>
public async Task RollbackAsync()
{
if (this.Result != null)
{
try
{
// delete the inserted row
await this.CustomerPurchasesRepository.DeleteAsync(this.Result);
}
catch (Exception deletionProblem)
{
if (deletionProblem.IsFatal())
{
throw;
}
Trace.TraceError(
"RecordPurchase.RollbackAsync failed: {0}, Customer ID: {1}, ID: {2}",
deletionProblem,
this.Result.CustomerId,
this.Result.Id);
// TODO: Notify the system integrity recovery component
}
this.Result = null;
}
}
}
}

Просмотреть файл

@ -0,0 +1,120 @@
// -----------------------------------------------------------------------
// <copyright file="RenewSubscription.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce.Transactions
{
using System;
using System.Diagnostics;
using System.Globalization;
using System.Threading.Tasks;
using Exceptions;
using Infrastructure;
using PartnerCenter.Exceptions;
using PartnerCenter.Models.Subscriptions;
using Subscriptions;
/// <summary>
/// Renews a partner center subscription.
/// </summary>
public class RenewSubscription : IBusinessTransactionWithOutput<Subscription>
{
/// <summary>
/// The existing subscription.
/// </summary>
private Subscription existingSubscription;
/// <summary>
/// Initializes a new instance of the <see cref="RenewSubscription"/> class.
/// </summary>
/// <param name="subscriptionOperations">A Partner Center subscription operations instance.</param>
/// <param name="existingSubscription">An existing subscription to update.</param>
public RenewSubscription(ISubscription subscriptionOperations, Subscription existingSubscription)
{
subscriptionOperations.AssertNotNull(nameof(subscriptionOperations));
existingSubscription.AssertNotNull(nameof(existingSubscription));
this.SubscriptionOperations = subscriptionOperations;
this.existingSubscription = existingSubscription;
}
/// <summary>
/// Gets the subscription operations used to manipulate the subscription.
/// </summary>
public ISubscription SubscriptionOperations { get; private set; }
/// <summary>
/// Gets the updated subscription.
/// </summary>
public Subscription Result { get; private set; }
/// <summary>
/// Purchases additional seats for the subscription.
/// </summary>
/// <returns>A task.</returns>
public async Task ExecuteAsync()
{
try
{
// activate the subscription (in case it was suspended)
this.Result = await this.SubscriptionOperations.PatchAsync(new Subscription()
{
Status = SubscriptionStatus.Active
});
}
catch (PartnerException subscriptionUpdateProblem)
{
string exceptionMessage = string.Format(
CultureInfo.InvariantCulture,
Resources.RenewSubscriptionFailedMessage,
subscriptionUpdateProblem,
this.existingSubscription.Id);
if (subscriptionUpdateProblem.ErrorCategory == PartnerErrorCategory.NotFound)
{
throw new PartnerDomainException(ErrorCode.SubscriptionNotFound, exceptionMessage, subscriptionUpdateProblem);
}
else
{
throw new PartnerDomainException(ErrorCode.SubscriptionUpdateFailure, exceptionMessage, subscriptionUpdateProblem);
}
}
}
/// <summary>
/// Reverts the subscription renewal.
/// </summary>
/// <returns>A task.</returns>
public async Task RollbackAsync()
{
if (this.Result != null)
{
try
{
// restore the original subscription state
await this.SubscriptionOperations.PatchAsync(this.existingSubscription);
}
catch (Exception rollbackProblem)
{
if (rollbackProblem.IsFatal())
{
throw;
}
Trace.TraceError(
"RenewSubscription.RollbackAsync failed: {0}, Customer ID: {1}, Subscription ID: {2}, Subscription: {3}",
rollbackProblem,
this.SubscriptionOperations.Context.Item1,
this.SubscriptionOperations.Context.Item2,
this.existingSubscription);
// TODO: Notify the system integrity recovery component
}
}
this.Result = null;
}
}
}

Просмотреть файл

@ -0,0 +1,110 @@
// -----------------------------------------------------------------------
// <copyright file="UpdatePersistedSubscription.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Commerce.Transactions
{
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Exceptions;
using Infrastructure;
using Models;
/// <summary>
/// Updates a subscription in persistence.
/// </summary>
public class UpdatePersistedSubscription : IBusinessTransactionWithOutput<CustomerSubscriptionEntity>
{
/// <summary>
/// The customer subscriptions repository used for accessing persistence.
/// </summary>
private CustomerSubscriptionsRepository repository;
/// <summary>
/// The required updates to the subscription.
/// </summary>
private CustomerSubscriptionEntity desiredSubscriptionUpdates;
/// <summary>
/// The subscription state before our update.
/// </summary>
private CustomerSubscriptionEntity originalSubscriptionState;
/// <summary>
/// Initializes a new instance of the <see cref="UpdatePersistedSubscription"/> class.
/// </summary>
/// <param name="repository">The payment gateway to use for authorization.</param>
/// <param name="updatedSubscriptionInformation">The updates to apply to the subscription in persistence.</param>
public UpdatePersistedSubscription(CustomerSubscriptionsRepository repository, CustomerSubscriptionEntity updatedSubscriptionInformation)
{
repository.AssertNotNull(nameof(repository));
updatedSubscriptionInformation.AssertNotNull(nameof(updatedSubscriptionInformation));
this.repository = repository;
this.desiredSubscriptionUpdates = updatedSubscriptionInformation;
}
/// <summary>
/// Gets the updated subscription entity.
/// </summary>
public CustomerSubscriptionEntity Result { get; private set; }
/// <summary>
/// Authorizes the payment amount.
/// </summary>
/// <returns>A task.</returns>
public async Task ExecuteAsync()
{
// retrieve the subscription
var customerSubscriptions = await this.repository.RetrieveAsync(this.desiredSubscriptionUpdates.CustomerId);
this.originalSubscriptionState = customerSubscriptions.Where(subscription => subscription.SubscriptionId == this.desiredSubscriptionUpdates.SubscriptionId).FirstOrDefault();
if (this.originalSubscriptionState == null)
{
throw new PartnerDomainException(ErrorCode.SubscriptionNotFound);
}
// update the subscription
this.Result = await this.repository.UpdateAsync(this.desiredSubscriptionUpdates);
}
/// <summary>
/// Rolls back the authorization.
/// </summary>
/// <returns>A task.</returns>
public async Task RollbackAsync()
{
if (this.Result != null)
{
try
{
// restore the subscription to what it was before
await this.repository.UpdateAsync(this.originalSubscriptionState);
}
catch (Exception restoreProblem)
{
if (restoreProblem.IsFatal())
{
throw;
}
Trace.TraceError(
"UpdatePersistedSubscription.RollbackAsync failed: {0}, Customer ID: {1}, ExpiryDate: {2}, PartnerOfferId: {3}, SubscriptionId: {4}",
restoreProblem,
this.originalSubscriptionState.CustomerId,
this.originalSubscriptionState.ExpiryDate,
this.originalSubscriptionState.PartnerOfferId,
this.originalSubscriptionState.SubscriptionId);
// TODO: Notify the system integrity recovery component
}
}
this.Result = null;
}
}
}

Просмотреть файл

@ -0,0 +1,74 @@
// -----------------------------------------------------------------------
// <copyright file="CustomerPortalPrincipal.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic
{
using System.Security.Claims;
using Configuration;
/// <summary>
/// Encapsulates relevant information about the logged in user.
/// </summary>
public class CustomerPortalPrincipal : ClaimsPrincipal
{
/// <summary>
/// Initializes a new instance of the <see cref="CustomerPortalPrincipal"/> class.
/// </summary>
/// <param name="userClaimsPrincipal">A user claims principal created by AAD.</param>
public CustomerPortalPrincipal(ClaimsPrincipal userClaimsPrincipal) : base(userClaimsPrincipal)
{
this.TenantId = userClaimsPrincipal.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid")?.Value;
this.Name = userClaimsPrincipal.FindFirst(ClaimTypes.Name)?.Value;
this.Email = userClaimsPrincipal.FindFirst(ClaimTypes.Email)?.Value;
// the customer ID will be empty in the case where a new prospective customer signs in with their existing Org ID or when a partner user signs in
this.PartnerCenterCustomerId = userClaimsPrincipal.FindFirst("PartnerCenterCustomerID")?.Value;
}
/// <summary>
/// Gets the AAD tenant ID of the signed in user.
/// </summary>
public string TenantId { get; private set; }
/// <summary>
/// Gets the name of the signed in user.
/// </summary>
public string Name { get; private set; }
/// <summary>
/// Gets the email of the signed in user.
/// </summary>
public string Email { get; private set; }
/// <summary>
/// Gets a value indicating whether the signed in user is a portal administrator or not.
/// </summary>
public bool IsPortalAdmin
{
get
{
// TODO: later on, we may want to implement RBAC but as of now, all users signed in from the portal's tenant are considered admins
return this.TenantId == ApplicationConfiguration.ActiveDirectoryTenantId;
}
}
/// <summary>
/// Gets a value indicating whether the sign in user is a current Partner Center customer or not.
/// </summary>
public bool IsPartnerCenterCustomer
{
get
{
return !string.IsNullOrEmpty(this.PartnerCenterCustomerId);
}
}
/// <summary>
/// Gets the Partner Center customer ID associated with the sign in user (if any).
/// </summary>
public string PartnerCenterCustomerId { get; private set; }
}
}

Просмотреть файл

@ -0,0 +1,29 @@
// -----------------------------------------------------------------------
// <copyright file="DomainObject.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic
{
/// <summary>
/// The base class for all domain objects.
/// </summary>
public abstract class DomainObject
{
/// <summary>
/// Initializes a new instance of the <see cref="DomainObject"/> class.
/// </summary>
/// <param name="applicationDomain">An application domain instance.</param>
protected DomainObject(ApplicationDomain applicationDomain)
{
applicationDomain.AssertNotNull(nameof(applicationDomain));
this.ApplicationDomain = applicationDomain;
}
/// <summary>
/// Gets the application domain instance hosting all domain services.
/// </summary>
protected ApplicationDomain ApplicationDomain { get; private set; }
}
}

Просмотреть файл

@ -0,0 +1,109 @@
// -----------------------------------------------------------------------
// <copyright file="ErrorCode.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Exceptions
{
/// <summary>
/// Categorizes errors that can happen during business domain operations.
/// </summary>
public enum ErrorCode
{
/// <summary>
/// The server had a failure it can't understand.
/// </summary>
ServerError,
/// <summary>
/// The resource already exists.
/// </summary>
AlreadyExists,
/// <summary>
/// An invalid set of inputs was provided.
/// </summary>
InvalidInput,
/// <summary>
/// A dependent service has failed.
/// </summary>
DownstreamServiceError,
/// <summary>
/// The given subscription could not be found.
/// </summary>
SubscriptionNotFound,
/// <summary>
/// The subscription is expired.
/// </summary>
SubscriptionExpired,
/// <summary>
/// The subscription could not be updated.
/// </summary>
SubscriptionUpdateFailure,
/// <summary>
/// The requested partner offer was not found.
/// </summary>
PartnerOfferNotFound,
/// <summary>
/// Failure in accessing persistence.
/// </summary>
PersistenceFailure,
/// <summary>
/// Unexpected file type.
/// </summary>
InvalidFileType,
/// <summary>
/// Failure in payment gateway.
/// </summary>
PaymentGatewayFailure,
/// <summary>
/// A failure due to an attempt to update the Microsoft offer associated with a partner offer.
/// </summary>
MicrosoftOfferImmutable,
/// <summary>
/// Maximum request size exceeded error.
/// </summary>
MaximumRequestSizeExceeded,
/// <summary>
/// Invalid address.
/// </summary>
InvalidAddress,
/// <summary>
/// Domain not available.
/// </summary>
DomainNotAvailable,
/// <summary>
/// Purchasing a delete offer.
/// </summary>
PurchaseDeletedOfferNotAllowed,
/// <summary>
/// Failure in payment gateway authentication during configuration.
/// </summary>
PaymentGatewayIdentityFailureDuringConfiguration,
/// <summary>
/// Failure in payment gateway authentication during payment.
/// </summary>
PaymentGatewayIdentityFailureDuringPayment,
/// <summary>
/// Failure in payment gateway during payment.
/// </summary>
PaymentGatewayPaymentError
}
}

Просмотреть файл

@ -0,0 +1,95 @@
// -----------------------------------------------------------------------
// <copyright file="PartnerDomainException.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Exceptions
{
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
/// <summary>
/// A custom exception that will be used to communicate business errors within the portal.
/// </summary>
[Serializable]
public class PartnerDomainException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="PartnerDomainException"/> class.
/// </summary>
public PartnerDomainException()
: base()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PartnerDomainException"/> class.
/// </summary>
/// <param name="message">The exception message.</param>
public PartnerDomainException(string message)
: this(message, null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PartnerDomainException"/> class.
/// </summary>
/// <param name="message">The exception message.</param>
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
public PartnerDomainException(string message, Exception innerException)
: base(message, innerException)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PartnerDomainException"/> class.
/// </summary>
/// <param name="errorCode">The error code.</param>
/// <param name="message">The exception message.</param>
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
public PartnerDomainException(ErrorCode errorCode, string message = default(string), Exception innerException = null)
: this(message, innerException)
{
ErrorCode = errorCode;
}
/// <summary>
/// Initializes a new instance of the <see cref="PartnerDomainException"/> class.
/// </summary>
/// <param name="info">The <see cref="SerializationInfo"/> that holds the serialized object data about the exception being thrown.</param>
/// <param name="context">The <see cref="StreamingContext"/> that contains contextual information about the source or destination.</param>
protected PartnerDomainException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
Details = (Dictionary<string, string>)info.GetValue("Details", typeof(Dictionary<string, string>));
ErrorCode = (ErrorCode)info.GetValue("ErrorCode", typeof(ErrorCode));
}
/// <summary>
/// Gets the error code which specifies what happened in the business area that caused the error.
/// </summary>
public ErrorCode ErrorCode { get; private set; } = ErrorCode.ServerError;
/// <summary>
/// Gets a dictionary of the error details.
/// </summary>
public IDictionary<string, string> Details { get; private set; } = new Dictionary<string, string>();
/// <summary>
/// Required override to add in the serialized parameters.
/// </summary>
/// <param name="info">Serialization information.</param>
/// <param name="context">Streaming context.</param>
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AssertNotNull(nameof(info));
info.AddValue("ErrorCode", ErrorCode);
info.AddValue("Details", Details);
base.GetObjectData(info, context);
}
}
}

Просмотреть файл

@ -0,0 +1,150 @@
// -----------------------------------------------------------------------
// <copyright file="GraphClient.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Graph;
using Models;
using Security;
/// <summary>
/// Provides the ability to interact with the Microsoft Graph.
/// </summary>
/// <seealso cref="IGraphClient" />
public class GraphClient : IGraphClient
{
/// <summary>
/// Static instance of the <see cref="HttpProvider" /> class.
/// </summary>
private static HttpProvider httpProvider = new HttpProvider(new HttpClientHandler(), false);
/// <summary>
/// Provides access to the Microsoft Graph.
/// </summary>
private readonly IGraphServiceClient client;
/// <summary>
/// Identifier of the customer.
/// </summary>
private readonly string customerId;
/// <summary>
/// Initializes a new instance of the <see cref="GraphClient"/> class.
/// </summary>
/// <param name="customerId">Identifier for customer whose resources are being accessed.</param>
/// <param name="authorizationCode">The authorization code received from service authorization endpoint.</param>
/// <param name="redirectUri">Address to return to upon receiving a response from the authority.</param>
/// <exception cref="ArgumentException">
/// <paramref name="customerId"/> is empty or null.
/// or
/// <paramref name="authorizationCode"/> is empty or null.
/// </exception>
/// <exception cref="ArgumentNullException">
/// <paramref name="redirectUri"/> is null.
/// </exception>"
public GraphClient(string customerId, string authorizationCode, Uri redirectUri)
{
customerId.AssertNotEmpty(nameof(customerId));
authorizationCode.AssertNotEmpty(nameof(authorizationCode));
this.customerId = customerId;
client = new GraphServiceClient(
new AuthenticationProvider(
customerId,
authorizationCode,
redirectUri),
httpProvider);
}
/// <summary>
/// Gets a list of roles assigned to the specified object identifier.
/// </summary>
/// <param name="objectId">Object identifier for the object to be checked.</param>
/// <returns>A list of roles that that are associated with the specified object identifier.</returns>
/// <exception cref="ArgumentException">
/// <paramref name="objectId"/> is empty or null.
/// </exception>
public async Task<List<RoleModel>> GetDirectoryRolesAsync(string objectId)
{
DateTime executionTime;
Dictionary<string, double> eventMeasurements;
Dictionary<string, string> eventProperties;
IUserMemberOfCollectionWithReferencesPage directoryGroups;
List<RoleModel> roles;
List<DirectoryRole> directoryRoles;
bool morePages;
objectId.AssertNotEmpty(nameof(objectId));
try
{
executionTime = DateTime.Now;
directoryGroups = await client.Users[objectId].MemberOf.Request().GetAsync().ConfigureAwait(false);
roles = new List<RoleModel>();
do
{
directoryRoles = directoryGroups.CurrentPage.OfType<DirectoryRole>().ToList();
if (directoryRoles.Count > 0)
{
roles.AddRange(directoryRoles.Select(r => new RoleModel
{
Description = r.Description,
DisplayName = r.DisplayName
}));
}
morePages = directoryGroups.NextPageRequest != null;
if (morePages)
{
directoryGroups = await directoryGroups.NextPageRequest.GetAsync().ConfigureAwait(false);
}
}
while (morePages);
// Capture the request for the customer summary for analysis.
eventProperties = new Dictionary<string, string>
{
{ "CustomerId", customerId },
{ "ObjectId", objectId }
};
// Track the event measurements for analysis.
eventMeasurements = new Dictionary<string, double>
{
{ "ElapsedMilliseconds", DateTime.Now.Subtract(executionTime).TotalMilliseconds },
{ "NumberOfRoles", roles.Count }
};
ApplicationDomain.Instance.TelemetryService.Provider.TrackEvent(nameof(GetDirectoryRolesAsync), eventProperties, eventMeasurements);
return roles;
}
catch (Exception ex)
{
ApplicationDomain.Instance.TelemetryService.Provider.TrackException(ex);
return null;
}
finally
{
directoryGroups = null;
directoryRoles = null;
eventMeasurements = null;
eventProperties = null;
}
}
}
}

Просмотреть файл

@ -0,0 +1,25 @@
// -----------------------------------------------------------------------
// <copyright file="IGraphClient.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic
{
using System.Collections.Generic;
using System.Threading.Tasks;
using Models;
/// <summary>
/// Represents an object that interacts with Microsoft Azure AD Graph API.
/// </summary>
public interface IGraphClient
{
/// <summary>
/// Gets a list of roles assigned to the specified object identifier.
/// </summary>
/// <param name="objectId">Object identifier for the object to be checked.</param>
/// <returns>A list of roles that that are associated with the specified object identifier.</returns>
Task<List<RoleModel>> GetDirectoryRolesAsync(string objectId);
}
}

Просмотреть файл

@ -0,0 +1,212 @@
// -----------------------------------------------------------------------
// <copyright file="MicrosoftOfferLogoIndexer.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Offers
{
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using PartnerCenter.Models.Offers;
using RequestContext;
/// <summary>
/// Indexes Microsoft offers and associated them with logo images.
/// </summary>
public class MicrosoftOfferLogoIndexer : DomainObject
{
/// <summary>
/// The default logo URI to use.
/// </summary>
private const string DefaultLogo = "/Content/Images/Plugins/ProductLogos/microsoft-logo.png";
/// <summary>
/// A collection of registered offer logo matchers.
/// </summary>
private readonly ICollection<IOfferLogoMatcher> offerLogoMatchers = new List<IOfferLogoMatcher>();
/// <summary>
/// A hash table mapping offer product IDs to their respective logo images.
/// </summary>
private readonly IDictionary<string, string> offerLogosIndex = new Dictionary<string, string>();
/// <summary>
/// Indicates whether offers have been indexed or not.
/// </summary>
private bool isIndexed = false;
/// <summary>
/// The time the index was last built.
/// </summary>
private DateTime lastIndexedTime;
/// <summary>
/// Initializes a new instance of the <see cref="MicrosoftOfferLogoIndexer"/> class.
/// </summary>
/// <param name="applicationDomain">An application domain instance.</param>
public MicrosoftOfferLogoIndexer(ApplicationDomain applicationDomain) : base(applicationDomain)
{
// register offer logo matchers
this.offerLogoMatchers.Add(new OfferLogoMatcher(new string[] { "azure", "active directory" }, "/Content/Images/Plugins/ProductLogos/azure-logo.png"));
this.offerLogoMatchers.Add(new OfferLogoMatcher(new string[] { "dynamics", "crm" }, "/Content/Images/Plugins/ProductLogos/dynamics-logo.png"));
this.offerLogoMatchers.Add(new OfferLogoMatcher(new string[] { "exchange" }, "/Content/Images/Plugins/ProductLogos/exchange-logo.png"));
this.offerLogoMatchers.Add(new OfferLogoMatcher(new string[] { "intune" }, "/Content/Images/Plugins/ProductLogos/intune-logo.png"));
this.offerLogoMatchers.Add(new OfferLogoMatcher(new string[] { "onedrive" }, "/Content/Images/Plugins/ProductLogos/onedrive-logo.png"));
this.offerLogoMatchers.Add(new OfferLogoMatcher(new string[] { "project" }, "/Content/Images/Plugins/ProductLogos/project-logo.png"));
this.offerLogoMatchers.Add(new OfferLogoMatcher(new string[] { "sharepoint" }, "/Content/Images/Plugins/ProductLogos/sharepoint-logo.png"));
this.offerLogoMatchers.Add(new OfferLogoMatcher(new string[] { "skype" }, "/Content/Images/Plugins/ProductLogos/skype-logo.png"));
this.offerLogoMatchers.Add(new OfferLogoMatcher(new string[] { "visio" }, "/Content/Images/Plugins/ProductLogos/visio-logo.png"));
this.offerLogoMatchers.Add(new OfferLogoMatcher(new string[] { "office", "365" }, "/Content/Images/Plugins/ProductLogos/office-logo.png"));
this.offerLogoMatchers.Add(new OfferLogoMatcher(new string[] { "yammer" }, "/Content/Images/Plugins/ProductLogos/yammer-logo.png"));
// we will default the logo if all the above matchers fail to match the given offer
this.offerLogoMatchers.Add(new DefaultLogoMatcher());
}
/// <summary>
/// The contract for an offer logo matcher.
/// </summary>
private interface IOfferLogoMatcher
{
/// <summary>
/// Attempts to match and offer with a logo URI.
/// </summary>
/// <param name="offer">The Microsoft offer to find its logo.</param>
/// <returns>The logo URI if matched. Empty string is could not match.</returns>
string Match(Offer offer);
}
/// <summary>
/// Returns a logo URI for the given offer.
/// </summary>
/// <param name="offer">The Microsoft offer to retrieve its logo.</param>
/// <returns>The offer's logo URI.</returns>
public async Task<string> GetOfferLogoUriAsync(Offer offer)
{
if (!this.isIndexed)
{
await this.IndexOffersAsync();
}
else
{
if (DateTime.Now - this.lastIndexedTime > TimeSpan.FromDays(1))
{
// it has been more than a day since we last indexed, reindex the next time this is called
this.isIndexed = false;
}
}
return offer?.Product?.Id != null && this.offerLogosIndex.ContainsKey(offer.Product.Id) ? this.offerLogosIndex[offer.Product.Id] : MicrosoftOfferLogoIndexer.DefaultLogo;
}
/// <summary>
/// Indexes offers with their respective logos.
/// </summary>
/// <returns>A task.</returns>
private async Task IndexOffersAsync()
{
// Need to manage this based on the partner's country locale to retrieve localized offers for the store front.
IPartner localeSpecificPartnerCenterClient = this.ApplicationDomain.PartnerCenterClient.With(RequestContextFactory.Instance.Create(this.ApplicationDomain.PortalLocalization.OfferLocale));
// retrieve the offers for this country
PartnerCenter.Models.ResourceCollection<Offer> localizedOffers = await localeSpecificPartnerCenterClient.Offers.ByCountry(this.ApplicationDomain.PortalLocalization.CountryIso2Code).GetAsync();
foreach (Offer offer in localizedOffers.Items)
{
if (offer?.Product?.Id != null && this.offerLogosIndex.ContainsKey(offer.Product.Id))
{
// this offer product has already been indexed, skip it
continue;
}
foreach (IOfferLogoMatcher offerLogoMatcher in this.offerLogoMatchers)
{
string logo = offerLogoMatcher.Match(offer);
if (!string.IsNullOrWhiteSpace(logo))
{
// logo matched, add it to the index
this.offerLogosIndex.Add(offer.Product.Id, logo);
break;
}
}
}
this.isIndexed = true;
this.lastIndexedTime = DateTime.Now;
}
/// <summary>
/// An offer logo matcher implementation that matches the offer product name against a set of keywords.
/// </summary>
private class OfferLogoMatcher : IOfferLogoMatcher
{
/// <summary>
/// The logo to use in case the offer was matched.
/// </summary>
private readonly string logo;
/// <summary>
/// The list of keywords to match against.
/// </summary>
private readonly IReadOnlyList<string> keywords;
/// <summary>
/// Initializes a new instance of the <see cref="OfferLogoMatcher"/> class.
/// </summary>
/// <param name="keywords">The keywords to match the offer product name against.</param>
/// <param name="logo">The offer logo to use in case of a match.</param>
public OfferLogoMatcher(IEnumerable<string> keywords, string logo)
{
keywords.AssertNotNull(nameof(keywords));
logo.AssertNotEmpty("logo URI can't be empty");
this.logo = logo;
this.keywords = new List<string>(keywords);
}
/// <summary>
/// Matches the given offer against the configured keywords.
/// </summary>
/// <param name="offer">The offer to match.</param>
/// <returns>The logo image if matched. Empty string if not.</returns>
public string Match(Offer offer)
{
offer.AssertNotNull(nameof(offer));
string offerName = offer.Name?.ToUpperInvariant();
if (!string.IsNullOrWhiteSpace(offerName))
{
foreach (string keyword in keywords)
{
if (offerName.Contains(keyword.ToUpperInvariant()))
{
return logo;
}
}
}
return string.Empty;
}
}
/// <summary>
/// An implementation that always returns a default logo for any given offer.
/// </summary>
private class DefaultLogoMatcher : IOfferLogoMatcher
{
/// <summary>
/// Matches an offer with a logo.
/// </summary>
/// <param name="offer">The offer to find its logo.</param>
/// <returns>The default logo</returns>
public string Match(Offer offer)
{
return DefaultLogo;
}
}
}
}

Просмотреть файл

@ -0,0 +1,79 @@
// -----------------------------------------------------------------------
// <copyright file="PartnerOfferNormalizer.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Offers
{
using System;
using System.Collections.Generic;
using Exceptions;
using Models;
/// <summary>
/// Applies business rules to a partner offer.
/// </summary>
public class PartnerOfferNormalizer
{
/// <summary>
/// Applies business rules to a partner offer.
/// </summary>
/// <param name="partnerOffer">The partner offer to normalize.</param>
public void Normalize(PartnerOffer partnerOffer)
{
partnerOffer.AssertNotNull(nameof(partnerOffer));
// ensure the Microsoft offer ID and other required properties are set
Guid offerId;
if (!Guid.TryParse(partnerOffer.Id, out offerId))
{
throw new PartnerDomainException(ErrorCode.InvalidInput, Resources.IdMustBeAValidGUID).AddDetail("Field", "Id");
}
if (string.IsNullOrWhiteSpace(partnerOffer.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, Resources.OfferPriceShouldBeMoreThanZero).AddDetail("Field", "Price");
}
// flatten the offer price based on locale decimal settings.
partnerOffer.Price = Math.Round(partnerOffer.Price, Resources.Culture.NumberFormat.CurrencyDecimalDigits, MidpointRounding.AwayFromZero);
partnerOffer.Features = PartnerOfferNormalizer.CleanupEmptyEntries(partnerOffer.Features);
partnerOffer.Summary = PartnerOfferNormalizer.CleanupEmptyEntries(partnerOffer.Summary);
}
/// <summary>
/// Removes empty elements from a given enumerable.
/// </summary>
/// <param name="enumerable">The enumerable to clean up.</param>
/// <returns>The cleaned up enumerable.</returns>
private static IEnumerable<string> CleanupEmptyEntries(IEnumerable<string> enumerable)
{
if (enumerable == null)
{
return null;
}
ICollection<string> filteredList = new List<string>();
foreach (var element in enumerable)
{
if (!string.IsNullOrWhiteSpace(element))
{
filteredList.Add(element);
}
}
return filteredList;
}
}
}

Просмотреть файл

@ -0,0 +1,290 @@
// -----------------------------------------------------------------------
// <copyright file="PartnerOffersRepository.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic.Offers
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Exceptions;
using Models;
using Newtonsoft.Json;
using RequestContext;
using WindowsAzure.Storage.Blob;
/// <summary>
/// Encapsulates the behavior of offers a partner has configured to sell to their customers.
/// </summary>
public class PartnerOffersRepository : DomainObject
{
/// <summary>
/// The Microsoft offers key in the cache.
/// </summary>
private const string MicrosoftOffersCacheKey = "MicrosoftOffers";
/// <summary>
/// The partner offers key in the cache.
/// </summary>
private const string PartnerOffersCacheKey = "PartnerOffers";
/// <summary>
/// The Azure BLOB name for the partner offers.
/// </summary>
private const string PartnerOffersBlobName = "partneroffers";
/// <summary>
/// Initializes a new instance of the <see cref="PartnerOffersRepository"/> class.
/// </summary>
/// <param name="applicationDomain">An application domain instance.</param>
public PartnerOffersRepository(ApplicationDomain applicationDomain) : base(applicationDomain)
{
}
/// <summary>
/// Checks if the partner offers had been configured or not.
/// </summary>
/// <returns>True if the offers were configured and stored, false otherwise.</returns>
public async Task<bool> IsConfiguredAsync()
{
return (await this.RetrieveAsync()).Where(offer => offer.IsInactive == false).Count() > 0;
}
/// <summary>
/// Fetches all Microsoft CSP offers.
/// </summary>
/// <returns>A list of all Microsoft CSP offers.</returns>
public async Task<IEnumerable<MicrosoftOffer>> RetrieveMicrosoftOffersAsync()
{
var microsoftOffers = await this.ApplicationDomain.CachingService
.FetchAsync<List<MicrosoftOffer>>(PartnerOffersRepository.MicrosoftOffersCacheKey);
if (microsoftOffers == null)
{
// Need to manage this based on the offer locale supported by the Offer API. Either its english or using one of the supported offer locale to retrieve localized offers for the store front.
var localeSpecificPartnerCenterClient = this.ApplicationDomain.PartnerCenterClient.With(RequestContextFactory.Instance.Create(this.ApplicationDomain.PortalLocalization.OfferLocale));
// Offers.ByCountry is required to pull country / region specific offers.
var partnerCenterOffers = await localeSpecificPartnerCenterClient.Offers.ByCountry(this.ApplicationDomain.PortalLocalization.CountryIso2Code).GetAsync();
var eligibleOffers = partnerCenterOffers?.Items.Where(offer =>
!offer.IsAddOn &&
(offer.PrerequisiteOffers == null || offer.PrerequisiteOffers.Count() <= 0)
&& offer.IsAvailableForPurchase == true);
microsoftOffers = new List<MicrosoftOffer>();
foreach (var partnerCenterOffer in eligibleOffers)
{
microsoftOffers.Add(new MicrosoftOffer()
{
Offer = partnerCenterOffer,
ThumbnailUri = new Uri(await this.ApplicationDomain.MicrosoftOfferLogoIndexer.GetOfferLogoUriAsync(partnerCenterOffer), UriKind.Relative)
});
}
// cache the Microsoft offers for one day
await this.ApplicationDomain.CachingService.StoreAsync<List<MicrosoftOffer>>(
PartnerOffersRepository.MicrosoftOffersCacheKey,
microsoftOffers,
TimeSpan.FromDays(1));
}
return microsoftOffers;
}
/// <summary>
/// Retrieves a specific partner offer using its ID.
/// </summary>
/// <param name="partnerOfferId">The ID of the partner offer to look for.</param>
/// <returns>The matching partner offer.</returns>
public async Task<PartnerOffer> RetrieveAsync(string partnerOfferId)
{
partnerOfferId.AssertNotEmpty(nameof(partnerOfferId));
PartnerOffer matchingPartnerOffer = (await this.RetrieveAsync()).Where(offer => offer.Id == partnerOfferId).FirstOrDefault();
if (matchingPartnerOffer != null)
{
return matchingPartnerOffer;
}
else
{
throw new PartnerDomainException(ErrorCode.PartnerOfferNotFound, Resources.OfferNotFound);
}
}
/// <summary>
/// Retrieves all the partner offers from persistence.
/// </summary>
/// <returns>The partner offers.</returns>
public async Task<IEnumerable<PartnerOffer>> RetrieveAsync()
{
var partnerOffers = await this.ApplicationDomain.CachingService
.FetchAsync<List<PartnerOffer>>(PartnerOffersRepository.PartnerOffersCacheKey);
if (partnerOffers == null)
{
var partnerOffersBlob = await this.GetPartnerOffersBlobAsync();
if (await partnerOffersBlob.ExistsAsync())
{
// download the partner offer BLOB
MemoryStream partnerOffersStream = new MemoryStream();
await partnerOffersBlob.DownloadToStreamAsync(partnerOffersStream);
partnerOffersStream.Seek(0, SeekOrigin.Begin);
// deserialize the BLOB into a list of Partner offer objects
partnerOffers =
JsonConvert.DeserializeObject<List<PartnerOffer>>(await new StreamReader(partnerOffersStream).ReadToEndAsync());
if (partnerOffers != null && partnerOffers.Count > 0)
{
// apply business rules to the offers
PartnerOfferNormalizer offerNormalizer = new PartnerOfferNormalizer();
foreach (var partnerOffer in partnerOffers)
{
offerNormalizer.Normalize(partnerOffer);
}
}
}
partnerOffers = partnerOffers ?? new List<PartnerOffer>();
// cache the partner offers
await this.ApplicationDomain.CachingService.StoreAsync<List<PartnerOffer>>(
PartnerOffersRepository.PartnerOffersCacheKey,
partnerOffers);
}
return partnerOffers;
}
/// <summary>
/// Adds a new partner offer to the repository.
/// </summary>
/// <param name="newPartnerOffer">The partner offer to add.</param>
/// <returns>The added partner offer.</returns>
public async Task<PartnerOffer> AddAsync(PartnerOffer newPartnerOffer)
{
if (newPartnerOffer == null)
{
throw new ArgumentNullException(nameof(newPartnerOffer));
}
newPartnerOffer.Id = Guid.NewGuid().ToString();
ICollection<PartnerOffer> allPartnerOffers = new List<PartnerOffer>(await this.RetrieveAsync());
new PartnerOfferNormalizer().Normalize(newPartnerOffer);
allPartnerOffers.Add(newPartnerOffer);
await this.UpdateAsync(allPartnerOffers);
return newPartnerOffer;
}
/// <summary>
/// Updates an existing partner offer.
/// </summary>
/// <param name="partnerOfferUpdate">The partner offer to update.</param>
/// <returns>The updated partner offer.</returns>
public async Task<PartnerOffer> UpdateAsync(PartnerOffer partnerOfferUpdate)
{
if (partnerOfferUpdate == null)
{
throw new ArgumentNullException(nameof(partnerOfferUpdate));
}
IList<PartnerOffer> allPartnerOffers = new List<PartnerOffer>(await this.RetrieveAsync());
new PartnerOfferNormalizer().Normalize(partnerOfferUpdate);
var existingPartnerOffer = allPartnerOffers.Where(offer => offer.Id == partnerOfferUpdate.Id).FirstOrDefault();
if (existingPartnerOffer == null)
{
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, Resources.MicrosoftOfferImmutableErrorMessage);
}
allPartnerOffers[allPartnerOffers.IndexOf(existingPartnerOffer)] = partnerOfferUpdate;
await this.UpdateAsync(allPartnerOffers);
return partnerOfferUpdate;
}
/// <summary>
/// Marks the passed partner offers as deleted.
/// </summary>
/// <param name="partnerOffersToDelete">The partner offers to mark as deleted.</param>
/// <returns>The updated partner offers.</returns>
public async Task<IEnumerable<PartnerOffer>> MarkAsDeletedAsync(IEnumerable<PartnerOffer> partnerOffersToDelete)
{
partnerOffersToDelete.AssertNotNull(nameof(partnerOffersToDelete));
ICollection<PartnerOffer> allPartnerOffers = new List<PartnerOffer>(await this.RetrieveAsync());
// mark the provided offers are deleted
var matchedOffers = allPartnerOffers.Where(offer => partnerOffersToDelete.Where(offerToDelete => offerToDelete.Id == offer.Id).FirstOrDefault() != null);
foreach (var offerToDelete in matchedOffers)
{
offerToDelete.IsInactive = true;
}
return await this.UpdateAsync(allPartnerOffers.ToList());
}
/// <summary>
/// Updates persistence with new partner offers. Existing offers will be wiped.
/// </summary>
/// <param name="partnerOffers">A collection of new partner offers.</param>
/// <returns>The resulting partner offers.</returns>
private async Task<IEnumerable<PartnerOffer>> UpdateAsync(ICollection<PartnerOffer> partnerOffers)
{
partnerOffers.AssertNotNull(nameof(partnerOffers));
try
{
// overwrite the partner offers BLOB
var partnerOffersBlob = await this.GetPartnerOffersBlobAsync();
await partnerOffersBlob.UploadTextAsync(JsonConvert.SerializeObject(partnerOffers));
// invalidate the cache, we do not update it to avoid race condition between web instances
await this.ApplicationDomain.CachingService.ClearAsync(PartnerOffersRepository.PartnerOffersCacheKey);
}
catch (Exception blobAccessProblem)
{
if (blobAccessProblem.IsFatal())
{
throw;
}
throw new PartnerDomainException(ErrorCode.PersistenceFailure, Resources.FailedToUpdatePartnerOffersStore, blobAccessProblem);
}
// return the normalized offers
return partnerOffers;
}
/// <summary>
/// Retrieves the partner offers BLOB reference.
/// </summary>
/// <returns>The partner offers BLOB.</returns>
private async Task<CloudBlockBlob> GetPartnerOffersBlobAsync()
{
var portalAssetsBlobContainer = await this.ApplicationDomain.AzureStorageService.GetPrivateCustomerPortalAssetsBlobContainerAsync();
return portalAssetsBlobContainer.GetBlockBlobReference(PartnerOffersRepository.PartnerOffersBlobName);
}
}
}

Просмотреть файл

@ -0,0 +1,134 @@
// -----------------------------------------------------------------------
// <copyright file="PortalLocalization.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic
{
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
/// <summary>
/// Encapsulates locale information for the portal based on the partner's region.
/// </summary>
public class PortalLocalization : DomainObject
{
/// <summary>
/// Initializes a new instance of the <see cref="PortalLocalization"/> class.
/// </summary>
/// <param name="applicationDomain">An application domain instance.</param>
public PortalLocalization(ApplicationDomain applicationDomain) : base(applicationDomain)
{
}
/// <summary>
/// Gets the portal's country ISO2 code. E.g. US
/// </summary>
public string CountryIso2Code { get; private set; }
/// <summary>
/// Gets the portal's locale. E.g. En-US
/// </summary>
public string Locale { get; private set; }
/// <summary>
/// Gets the portal's ISO currency code. E.g. USD
/// </summary>
public string CurrencyCode { get; private set; }
/// <summary>
/// Gets the portal's currency symbol. E.g. $
/// </summary>
public string CurrencySymbol { get; private set; }
/// <summary>
/// Gets the portal's locale which is applied for offer API calls to partner center.
/// </summary>
public string OfferLocale { get; private set; }
/// <summary>
/// Initializes state and ensures the object is ready to be consumed.
/// </summary>
/// <returns>A task.</returns>
public async Task InitializeAsync()
{
var partnerLegalBusinessProfile = await this.ApplicationDomain.PartnerCenterClient.Profiles.LegalBusinessProfile.GetAsync().ConfigureAwait(false);
this.CountryIso2Code = partnerLegalBusinessProfile.Address.Country;
RegionInfo partnerRegion = null;
try
{
// Get the default locale using the Country Validation rules infrastructure.
var partnerCountryValidationRules = await ApplicationDomain.Instance.PartnerCenterClient.CountryValidationRules.ByCountry(CountryIso2Code).GetAsync().ConfigureAwait(false);
this.Locale = partnerCountryValidationRules.DefaultCulture;
partnerRegion = new RegionInfo(new CultureInfo(this.Locale, false).LCID);
}
catch
{
// we will default region to en-US so that currency is USD.
this.Locale = "en-US";
partnerRegion = new RegionInfo(new CultureInfo(this.Locale, false).LCID);
}
this.OfferLocale = this.ResolveOfferLocale(this.Locale);
// figure out the currency
this.CurrencyCode = partnerRegion.ISOCurrencySymbol;
this.CurrencySymbol = partnerRegion.CurrencySymbol;
// set culture to partner locale.
Resources.Culture = new CultureInfo(this.Locale);
}
/// <summary>
/// Resolver to identify the right locale settings which can be used for calling offer APIs.
/// </summary>
/// <param name="locale">Partner Locale</param>
/// <returns>Offer Locale</returns>
private string ResolveOfferLocale(string locale)
{
List<string> portalSupportedLocales = new List<string>
{
"de",
"en",
"es",
"nl",
"fr",
"ja"
};
List<string> portalOfferLocaleDefaults = new List<string>
{
"de-DE",
"en-US",
"es-ES",
"nl-NL",
"fr-FR",
"ja-JP"
};
//// Examples [en-US, en-GB, en-CA] -> en-US ==> en ==> (en-US), en-GB ==> en ==> (en-US), en-CA ==> en ==> (en-US).
//// Examples [fr-CH, de-CH, it-CH] -> fr-CH ==> fr ==> (fr-FR), de-CH ==> de ==> (de-DE), it-CH ==> it ==> (en-US).
// if language is not supported by Portal then default it to en-US since the portal runs on english.
string offerSpecificLocale = "en-US";
// if language is supported by Portal then default it to the portalOfferLocaleDefault
string languageInLocale = new CultureInfo(locale).TwoLetterISOLanguageName;
// check the language part and see if we can default to one of the top locales.
int localeIndex = portalSupportedLocales.IndexOf(languageInLocale);
if (localeIndex > -1)
{
offerSpecificLocale = portalOfferLocaleDefaults.ElementAt(localeIndex);
}
return offerSpecificLocale;
}
}
}

Просмотреть файл

@ -0,0 +1,70 @@
// -----------------------------------------------------------------------
// <copyright file="TelemetryService.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.BusinessLogic
{
using System.Threading.Tasks;
using Telemetry;
/// <summary>
/// Provides the ability to capture telemetry for portal.
/// </summary>
public class TelemetryService : DomainObject
{
/// <summary>
/// An instance of the appropriate telemetry provider for the portal.
/// </summary>
private static ITelemetryProvider telemetryProvider;
/// <summary>
/// Initializes a new instance of the <see cref="TelemetryService"/> class.
/// </summary>
/// <param name="applicationDomain">An application domain instance.</param>
public TelemetryService(ApplicationDomain applicationDomain) : base(applicationDomain)
{
}
/// <summary>
/// Gets the instrumentation key from the portal configuration.
/// </summary>
public string InstrumentationKey { get; private set; }
/// <summary>
/// Gets an instance of the appropriate telemetry provider.
/// </summary>
public ITelemetryProvider Provider
{
get
{
if (string.IsNullOrEmpty(this.InstrumentationKey))
{
telemetryProvider = new EmptyTelemetryProvider();
}
else
{
telemetryProvider = new ApplicationInsightsTelemetryProvider();
}
return telemetryProvider;
}
}
/// <summary>
/// Initializes the telemetry provider based upon the portal configuration.
/// </summary>
/// <returns>A task for asynchronous purposes.</returns>
public async Task InitializeAsync()
{
this.InstrumentationKey = (await ApplicationDomain.Instance.PortalBranding.RetrieveAsync().ConfigureAwait(false)).InstrumentationKey;
if (!string.IsNullOrEmpty(this.InstrumentationKey))
{
ApplicationInsights.Extensibility.TelemetryConfiguration.Active.InstrumentationKey =
this.InstrumentationKey;
}
}
}
}

Просмотреть файл

@ -0,0 +1,141 @@
// -----------------------------------------------------------------------
// <copyright file="ApplicationConfiguration.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.Configuration
{
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Web;
using System.Web.Configuration;
using BusinessLogic.Commerce.PaymentGateways;
using Manager;
/// <summary>
/// Abstracts the Web server configuration stored in different places such as web.config.
/// </summary>
public static class ApplicationConfiguration
{
/// <summary>
/// The AAD endpoint configuration key.
/// </summary>
private const string ActiveDirectoryEndPointKey = "aadEndpoint";
/// <summary>
/// The AD Graph endpoint configuration key.
/// </summary>
private const string ActiveDirectoryGraphEndPointKey = "aadGraphEndpoint";
/// <summary>
/// The web portal AD client ID configuration key.
/// </summary>
private const string WebPortalADClientIDKey = "webPortal.clientId";
/// <summary>
/// The web portal AD client secret configuration key.
/// </summary>
private const string WebPortalADClientSecretKey = "webPortal.clientSecret";
/// <summary>
/// The web portal AD tenant ID.
/// </summary>
private const string WebPortalAadTenantID = "webPortal.AadTenantId";
/// <summary>
/// The web portal configuration file path configuration key.
/// </summary>
private const string WebPortalConfigurationFilePathKey = "webPortal.configurationPath";
/// <summary>
/// The Azure storage connection string configuration key.
/// </summary>
private const string AzureStorageConnectionStringKey = "webPortal.azureStorageConnectionString";
/// <summary>
/// The Azure storage connection endpoint suffix key.
/// </summary>
private const string AzureStorageConnectionEndpointSuffixKey = "webPortal.azureStorageConnectionEndpointSuffix";
/// <summary>
/// The cache connection string configuration key.
/// </summary>
private const string CacheConnectionStringKey = "webPortal.cacheConnectionString";
/// <summary>
/// The web portal configuration manager configuration key.
/// </summary>
private const string WebPortalConfigurationManagerKey = "WebPortalConfigurationManager";
/// <summary>
/// A lazy reference to client configuration.
/// </summary>
private static Lazy<IDictionary<string, dynamic>> clientConfiguration = new Lazy<IDictionary<string, dynamic>>(
() => WebPortalConfigurationManager.GenerateConfigurationDictionary().Result);
/// <summary>
/// Gets the web portal configuration file path.
/// </summary>
public static string WebPortalConfigurationFilePath => Path.Combine(
HttpRuntime.AppDomainAppPath,
WebConfigurationManager.AppSettings[WebPortalConfigurationFilePathKey] + PaymentGatewayConfig.GetWebConfigPath());
/// <summary>
/// Gets the client configuration.
/// </summary>
public static IDictionary<string, dynamic> ClientConfiguration => clientConfiguration.Value;
/// <summary>
/// Gets or sets the web portal configuration manager instance.
/// </summary>
public static WebPortalConfigurationManager WebPortalConfigurationManager
{
get => HttpContext.Current.Application[WebPortalConfigurationManagerKey] as WebPortalConfigurationManager;
set => HttpContext.Current.Application[WebPortalConfigurationManagerKey] = value;
}
/// <summary>
/// Gets the Azure Active Directory endpoint used by the web portal.
/// </summary>
public static string ActiveDirectoryEndPoint => ConfigurationManager.AppSettings[ActiveDirectoryEndPointKey];
/// <summary>
/// Gets the Azure Active Directory Graph endpoint used by the web portal.
/// </summary>
public static string ActiveDirectoryGraphEndPoint => ConfigurationManager.AppSettings[ActiveDirectoryGraphEndPointKey];
/// <summary>
/// Gets the Azure Active Directory client ID of the web portal.
/// </summary>
public static string ActiveDirectoryClientID => ConfigurationManager.AppSettings[WebPortalADClientIDKey];
/// <summary>
/// Gets the Azure Active Directory client secret of the web portal.
/// </summary>
public static string ActiveDirectoryClientSecret => ConfigurationManager.AppSettings[WebPortalADClientSecretKey];
/// <summary>
/// Gets the Azure Active Directory ID of the web portal.
/// </summary>
public static string ActiveDirectoryTenantId => ConfigurationManager.AppSettings[WebPortalAadTenantID];
/// <summary>
/// Gets the Azure storage connection string.
/// </summary>
public static string AzureStorageConnectionString => ConfigurationManager.AppSettings[AzureStorageConnectionStringKey];
/// <summary>
/// Gets the Azure Azure storage endpoint suffix.
/// </summary>
public static string AzureStorageConnectionEndpointSuffix => ConfigurationManager.AppSettings[AzureStorageConnectionEndpointSuffixKey];
/// <summary>
/// Gets the cache connection string.
/// </summary>
public static string CacheConnectionString => ConfigurationManager.AppSettings[CacheConnectionStringKey];
}
}

Просмотреть файл

@ -0,0 +1,76 @@
// -----------------------------------------------------------------------
// <copyright file="Bundler.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.Configuration.Bundling
{
using System;
using System.Web.Optimization;
/// <summary>
/// Abstracts bundling client files.
/// </summary>
public sealed class Bundler
{
/// <summary>
/// The singleton bundler instance.
/// </summary>
private static Lazy<Bundler> instance = new Lazy<Bundler>(() => { return new Bundler(); });
/// <summary>
/// The bundles collection.
/// </summary>
private BundleCollection bundles;
/// <summary>
/// Prevents a default instance of the <see cref="Bundler"/> class from being created.
/// </summary>
private Bundler()
{
this.bundles = BundleTable.Bundles;
}
/// <summary>
/// Gets the bundler instance.
/// </summary>
public static Bundler Instance
{
get
{
return instance.Value;
}
}
/// <summary>
/// Clears all previously configured bundles.
/// </summary>
public void Clear()
{
this.bundles.Clear();
}
/// <summary>
/// Bundles startup assets.
/// </summary>
/// <param name="javaScriptFiles">The JS files to include in the startup bundle.</param>
/// <param name="cssFiles">The CSS files to include in the startup bundle.</param>
public void BundleStartupAssets(string[] javaScriptFiles, string[] cssFiles)
{
this.bundles.Add(new ScriptBundle("~/StartupClasses/").Include(javaScriptFiles));
this.bundles.Add(new StyleBundle("~/StartupStyles/").Include(cssFiles));
}
/// <summary>
/// Bundles non startup assets.
/// </summary>
/// <param name="javaScriptFiles">The JS files to include in the non startup bundle.</param>
/// <param name="cssFiles">The CSS files to include in the non startup bundle.</param>
public void BundleNonStartupAssets(string[] javaScriptFiles, string[] cssFiles)
{
this.bundles.Add(new ScriptBundle("~/WebPortalClasses/").Include(javaScriptFiles));
this.bundles.Add(new StyleBundle("~/WebPortalStyles/").Include(cssFiles));
}
}
}

Просмотреть файл

@ -0,0 +1,21 @@
// -----------------------------------------------------------------------
// <copyright file="IWebPortalConfigurationFactory.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.Configuration.Manager
{
/// <summary>
/// Creates <see cref="WebPortalConfigurationManager"/> instances.
/// </summary>
public interface IWebPortalConfigurationFactory
{
/// <summary>
/// Creates a new <see cref="WebPortalConfigurationManager"/> instance.
/// </summary>
/// <param name="configurationFilePath">The web portal configuration file path.</param>
/// <returns>A new web portal configuration manager instance.</returns>
WebPortalConfigurationManager Create(string configurationFilePath);
}
}

Просмотреть файл

@ -0,0 +1,149 @@
// -----------------------------------------------------------------------
// <copyright file="StandardWebPortalConfigurationManager.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.Configuration.Manager
{
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bundling;
using WebPortal;
/// <summary>
/// The standard web portal configuration manager. Reads the provided configuration file and serves the client with the configured asset versions.
/// </summary>
public class StandardWebPortalConfigurationManager : WebPortalConfigurationManager
{
/// <summary>
/// The aggregated portal startup assets.
/// </summary>
private readonly Lazy<Assets> startupAssets;
/// <summary>
/// The aggregated portal non startup assets.
/// </summary>
private readonly Lazy<Assets> nonStartupAssets;
/// <summary>
/// Indicates whether the bundles have been generated yet.
/// </summary>
private bool isBundlesGenerated = false;
/// <summary>
/// Initializes a new instance of the <see cref="StandardWebPortalConfigurationManager"/> class.
/// </summary>
/// <param name="configurationFilePath">The web portal configuration file path.</param>
public StandardWebPortalConfigurationManager(string configurationFilePath)
: base(configurationFilePath)
{
// we only want to generate the assets once since all users of the system will be served the same assets
this.startupAssets = new Lazy<Assets>(() => { return this.GenerateStartupAssets(); });
this.nonStartupAssets = new Lazy<Assets>(() => { return this.GenerateNonStartupAssets(); });
}
/// <summary>
/// Updates the web portal bundle files.
/// </summary>
/// <param name="bundler">The bundler instance.</param>
/// <returns>A task which is complete when the bundles are updated.</returns>
public override async Task UpdateBundles(Bundler bundler)
{
if (this.isBundlesGenerated)
{
// only generate the bundles once since they will remain constant across the web application's life span
return;
}
// call the standard bundling implementation
await base.UpdateBundles(bundler).ConfigureAwait(false);
this.isBundlesGenerated = true;
}
/// <summary>
/// Aggregates client side files which are needed during the portal start up.
/// </summary>
/// <returns>The aggregated startup assets.</returns>
public override async Task<Assets> AggregateStartupAssets()
{
// return the startup assests we had already built
return await Task.FromResult<Assets>(this.startupAssets.Value).ConfigureAwait(false);
}
/// <summary>
/// Aggregates client side files which are needed once the portal is up and running.
/// </summary>
/// <returns>The aggregated non startup assets.</returns>
public override async Task<Assets> AggregateNonStartupAssets()
{
// return the non startup assests we had already built
return await Task.FromResult<Assets>(this.nonStartupAssets.Value).ConfigureAwait(false);
}
/// <summary>
/// Generates the plugins which will be sent down to the client.
/// </summary>
/// <returns>The plugins configuration.</returns>
public override async Task<PluginsSegment> GeneratePlugins()
{
// return the plugin configuration as found in the configuration file
return (await Task.FromResult<PluginsSegment>(this.Configuration.Plugins).ConfigureAwait(false)).Clone() as PluginsSegment;
}
/// <summary>
/// Generates the configuration settings which will be sent down to the client.
/// </summary>
/// <returns>A dictionary of configuration settings.</returns>
public override async Task<Dictionary<string, dynamic>> GenerateConfigurationDictionary()
{
// return the configuration as is
return await Task.FromResult(Configuration.Configuration).ConfigureAwait(false);
}
/// <summary>
/// Builds the portal startup assets.
/// </summary>
/// <returns>The portal startup assets.</returns>
private Assets GenerateStartupAssets()
{
// aggregate asset files in this order: dependency, core startup
Assets dependencies = this.Configuration.Dependencies.Assets.GetAssetsByVersion(this.Configuration.Dependencies.DefaultAssetVersion);
Assets coreStartup = this.Configuration.Core.Startup.Assets.GetAssetsByVersion(this.Configuration.Core.Startup.DefaultAssetVersion);
return dependencies + coreStartup;
}
/// <summary>
/// Builds the portal non startup assets.
/// </summary>
/// <returns>The portal non startup assets.</returns>
private Assets GenerateNonStartupAssets()
{
// aggregate asset files in this order: core non startup, services, views, plugins
Assets nonStartup = this.Configuration.Core.NonStartup.Assets.GetAssetsByVersion(this.Configuration.Core.NonStartup.DefaultAssetVersion);
Assets services = this.Configuration.Services.AggregateAssets();
Assets views = this.Configuration.Views.AggregateAssets();
Assets plugins = this.Configuration.Plugins.Commons.Assets.GetAssetsByVersion(this.Configuration.Plugins.Commons.DefaultAssetVersion);
foreach (Plugin plugin in this.Configuration.Plugins.Plugins)
{
plugins += plugin.Features.AggregateAssets();
// the WebPortalConfiguration.json plugin/DisplayName attribute value is used as the key in the resource file.
string localizedPluginDisplayName = Resources.ResourceManager.GetString(plugin.DisplayName, Resources.Culture);
if (string.IsNullOrWhiteSpace(localizedPluginDisplayName))
{
// if resource is not available then just reuse the DisplayName in the json configuration.
localizedPluginDisplayName = plugin.DisplayName;
}
plugin.DisplayName = localizedPluginDisplayName;
}
return nonStartup + services + views + plugins;
}
}
}

Просмотреть файл

@ -0,0 +1,24 @@
// -----------------------------------------------------------------------
// <copyright file="WebPortalConfigurationFactory.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.Configuration.Manager
{
/// <summary>
/// The default web portal configuration factory implementation.
/// </summary>
public class WebPortalConfigurationFactory : IWebPortalConfigurationFactory
{
/// <summary>
/// Creates a new web portal configuration manager instance.
/// </summary>
/// <param name="configurationFilePath">The web portal configuration file path.</param>
/// <returns>A new web portal configuration manager instance.</returns>
public WebPortalConfigurationManager Create(string configurationFilePath)
{
return new StandardWebPortalConfigurationManager(configurationFilePath);
}
}
}

Просмотреть файл

@ -0,0 +1,99 @@
// -----------------------------------------------------------------------
// <copyright file="WebPortalConfigurationManager.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.Configuration.Manager
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Bundling;
using Newtonsoft.Json;
using WebPortal;
/// <summary>
/// The configuration manager generates the web portal client side configuration settings and is also responsible for generating and aggregating
/// client side files, plugins configuration and other settings.
/// </summary>
public abstract class WebPortalConfigurationManager
{
/// <summary>
/// Initializes a new instance of the <see cref="WebPortalConfigurationManager"/> class.
/// </summary>
/// <param name="configurationFilePath">The web portal configuration file path.</param>
protected WebPortalConfigurationManager(string configurationFilePath)
{
if (string.IsNullOrWhiteSpace(configurationFilePath))
{
throw new ArgumentException("configurationFilePath not set", nameof(configurationFilePath));
}
using (StreamReader configReader = new StreamReader(configurationFilePath))
{
// read and process the configuration
this.Configuration = JsonConvert.DeserializeObject<WebPortalConfiguration>(configReader.ReadToEnd());
this.Configuration.Process();
}
}
/// <summary>
/// Gets or sets the web portal configuration.
/// </summary>
protected WebPortalConfiguration Configuration { get; set; }
/// <summary>
/// Aggregates client side files which are needed during the portal start up.
/// </summary>
/// <returns>The aggregated startup assets.</returns>
public abstract Task<Assets> AggregateStartupAssets();
/// <summary>
/// Aggregates client side files which are needed once the portal is up and running.
/// </summary>
/// <returns>The aggregated non startup assets.</returns>
public abstract Task<Assets> AggregateNonStartupAssets();
/// <summary>
/// Generates the plugins which will be sent down to the client.
/// </summary>
/// <returns>The plugins configuration.</returns>
public abstract Task<PluginsSegment> GeneratePlugins();
/// <summary>
/// Generates the configuration settings which will be sent down to the client.
/// </summary>
/// <returns>A dictionary of configuration settings.</returns>
public abstract Task<Dictionary<string, dynamic>> GenerateConfigurationDictionary();
/// <summary>
/// Updates the web portal bundle files. This controls which files are bundled and sent to the client browser.
/// </summary>
/// <param name="bundler">The bundler instance.</param>
/// <returns>A task which is complete when the bundles are updated.</returns>
public async virtual Task UpdateBundles(Bundler bundler)
{
if (bundler == null)
{
throw new ArgumentNullException(nameof(bundler), "null bundler passed in");
}
bundler.Clear();
Assets startUpAssets = await AggregateStartupAssets().ConfigureAwait(false);
Assets nonStartUpAssets = await AggregateNonStartupAssets().ConfigureAwait(false);
// build the start up javascript and css files and bundle them
List<string> startupClasses = new List<string>(startUpAssets.JavaScript);
List<string> startupStyles = new List<string>(startUpAssets.Css);
bundler.BundleStartupAssets(startupClasses.ToArray(), startupStyles.ToArray());
// build the non startup files and bundle them
List<string> nonStartupClasses = new List<string>(nonStartUpAssets.JavaScript);
List<string> nonStartupStyles = new List<string>(nonStartUpAssets.Css);
bundler.BundleNonStartupAssets(nonStartupClasses.ToArray(), nonStartupStyles.ToArray());
}
}
}

Просмотреть файл

@ -0,0 +1,176 @@
// -----------------------------------------------------------------------
// <copyright file="Assets.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.Configuration.WebPortal
{
using System;
using System.Collections.Generic;
/// <summary>
/// Represents a collection of client asset files.
/// </summary>
public class Assets : ICloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="Assets"/> class.
/// </summary>
public Assets()
{
this.Css = new List<string>();
this.JavaScript = new List<string>();
this.Templates = new List<string>();
}
/// <summary>
/// Gets or sets the assets version.
/// </summary>
public string Version { get; set; }
/// <summary>
/// Gets or sets a collection of CSS file paths.
/// </summary>
public IEnumerable<string> Css { get; set; }
/// <summary>
/// Gets or sets a collection of JavaScript file paths.
/// </summary>
public IEnumerable<string> JavaScript { get; set; }
/// <summary>
/// Gets or sets a collection of HTML template URL routes.
/// </summary>
public IEnumerable<string> Templates { get; set; }
/// <summary>
/// Adds two <see cref="Assets"/> objects and returns the sum.
/// </summary>
/// <param name="left">The left side assets object.</param>
/// <param name="right">The right side assets object.</param>
/// <returns>A assets object which has the files of both operands appended.</returns>
public static Assets operator +(Assets left, Assets right)
{
if (left == null)
{
return right;
}
if (right == null)
{
return left;
}
// combine the assets together
List<string> combinedCss = new List<string>(left.Css);
combinedCss.AddRange(right.Css);
List<string> combinedJavaScript = new List<string>(left.JavaScript);
combinedJavaScript.AddRange(right.JavaScript);
List<string> combinedTemplates = new List<string>(left.Templates);
combinedTemplates.AddRange(right.Templates);
return new Assets() { Version = left.Version, Css = combinedCss, JavaScript = combinedJavaScript, Templates = combinedTemplates };
}
/// <summary>
/// Adds two <see cref="Assets"/> objects and returns the sum.
/// </summary>
/// <param name="left">The left side assets object.</param>
/// <param name="right">The right side assets object.</param>
/// <returns>A assets object which has the files of both operands appended.</returns>
public static Assets Add(Assets left, Assets right)
{
if (left == null)
{
return right;
}
if (right == null)
{
return left;
}
// combine the assets together
List<string> combinedCss = new List<string>(left.Css);
combinedCss.AddRange(right.Css);
List<string> combinedJavaScript = new List<string>(left.JavaScript);
combinedJavaScript.AddRange(right.JavaScript);
List<string> combinedTemplates = new List<string>(left.Templates);
combinedTemplates.AddRange(right.Templates);
return new Assets() { Version = left.Version, Css = combinedCss, JavaScript = combinedJavaScript, Templates = combinedTemplates };
}
/// <summary>
/// Clones an <see cref="Assets"/> instance.
/// </summary>
/// <returns>A deep copy of the assets object.</returns>
public object Clone()
{
Assets cloneAssets = new Assets() { Version = this.Version };
cloneAssets.Css = this.Css.Clone();
cloneAssets.JavaScript = this.JavaScript.Clone();
cloneAssets.Templates = this.Templates.Clone();
return cloneAssets;
}
/// <summary>
/// Ensures the assets object properties are in a good state.
/// </summary>
/// <exception cref="InvalidOperationException">If the asset properties are invalid.</exception>
public void Validate()
{
if (string.IsNullOrWhiteSpace(this.Version))
{
throw new InvalidOperationException("Asset version is not set.");
}
// ensure our asset collections are set to something
if (this.Css == null)
{
this.Css = new List<string>();
}
if (this.JavaScript == null)
{
this.JavaScript = new List<string>();
}
if (this.Templates == null)
{
this.Templates = new List<string>();
}
// validate asset collections to hold valid values
this.ValidateAssetCollections(this.Css);
this.ValidateAssetCollections(this.JavaScript);
this.ValidateAssetCollections(this.Templates);
}
/// <summary>
/// Ensures asset collections contain non empty strings.
/// </summary>
/// <param name="assetsCollection">The asset collection to validate.</param>
/// <exception cref="InvalidOperationException">If the asset properties are invalid.</exception>
private void ValidateAssetCollections(IEnumerable<string> assetsCollection)
{
if (assetsCollection != null)
{
foreach (string asset in assetsCollection)
{
if (string.IsNullOrWhiteSpace(asset))
{
throw new InvalidOperationException("Can't have an empty asset, please ensure all asset strings are set.");
}
}
}
}
}
}

Просмотреть файл

@ -0,0 +1,80 @@
// -----------------------------------------------------------------------
// <copyright file="AssetsSegment.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.Configuration.WebPortal
{
using System;
using System.Collections.Generic;
/// <summary>
/// Represents an assets segment in the portal configuration. This segment may hold more than one versioned asset sets.
/// </summary>
public class AssetsSegment : ICloneable
{
/// <summary>
/// Gets or sets the segment name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets or sets the default assets version to use. If not specified then the first asset in the collection will be used as the default.
/// </summary>
public string DefaultAssetVersion { get; set; }
/// <summary>
/// Gets or sets a collection of asset sets.
/// </summary>
public IList<Assets> Assets { get; set; }
/// <summary>
/// Ensures the assets segment is valid.
/// </summary>
/// <exception cref="InvalidOperationException">If validation fails.</exception>
public void Validate()
{
// ensure there are assets specified
if (this.Assets == null || this.Assets.Count <= 0)
{
throw new InvalidOperationException("Assets not set. Please specify at least one assets version.");
}
bool isDefaultAssetVersionValid = false;
if (string.IsNullOrWhiteSpace(this.DefaultAssetVersion))
{
// if no default asset version is specified, set it to the first assets set
this.DefaultAssetVersion = this.Assets[0].Version;
isDefaultAssetVersionValid = true;
}
// validate these assets
foreach (Assets assetSet in this.Assets)
{
assetSet.Validate();
if (!isDefaultAssetVersionValid && assetSet.Version == this.DefaultAssetVersion)
{
isDefaultAssetVersionValid = true;
}
}
// ensure the given default asset version is valid
if (!isDefaultAssetVersionValid)
{
throw new InvalidOperationException("Invalid default asset version.");
}
}
/// <summary>
/// Clones an <see cref="AssetsSegment"/> instance.
/// </summary>
/// <returns>A deep copy of the <see cref="AssetsSegment"/> object.</returns>
public object Clone()
{
return new AssetsSegment() { Name = this.Name, DefaultAssetVersion = this.DefaultAssetVersion, Assets = this.Assets.Clone() };
}
}
}

Просмотреть файл

@ -0,0 +1,93 @@
// -----------------------------------------------------------------------
// <copyright file="ConfigurationExtensions.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.Configuration.WebPortal
{
using System;
using System.Collections.Generic;
/// <summary>
/// Contains useful extension methods for configuration classes.
/// </summary>
public static class ConfigurationExtensions
{
/// <summary>
/// Clones a collection of cloneable objects.
/// </summary>
/// <typeparam name="T">The list object type.</typeparam>
/// <param name="collection">The list to clone.</param>
/// <returns>A deep copy of the collection.</returns>
public static IList<T> Clone<T>(this IEnumerable<T> collection)
where T : ICloneable
{
IList<T> cloneCollection = new List<T>();
foreach (var item in collection)
{
cloneCollection.Add((T)item.Clone());
}
return cloneCollection;
}
/// <summary>
/// Retrieves the assets from a list using its version.
/// </summary>
/// <param name="assetsList">The assets list to search.</param>
/// <param name="version">The version to look for.</param>
/// <returns>The matching assets or empty assets object in case it was not found.</returns>
public static Assets GetAssetsByVersion(this IList<Assets> assetsList, string version)
{
Assets assetsMatch = null;
if (assetsList != null && assetsList.Count > 0)
{
// search for the asset version
foreach (Assets assets in assetsList)
{
if (assets.Version == version)
{
assetsMatch = assets;
break;
}
}
if (assetsMatch == null)
{
// set if to the first entry if not found
assetsMatch = assetsList[0];
}
}
else
{
// no valid assets, return an empty asset set
assetsMatch = new Assets();
}
return assetsMatch;
}
/// <summary>
/// Aggregates an assets segment assets. Uses default asset versions.
/// </summary>
/// <param name="assetSegmentList">The assets segment list.</param>
/// <returns>Aggregated assets.</returns>
public static Assets AggregateAssets(this IEnumerable<AssetsSegment> assetSegmentList)
{
Assets combinedSegmentListAssets = new Assets();
if (assetSegmentList != null)
{
foreach (AssetsSegment segmentAssets in assetSegmentList)
{
combinedSegmentListAssets += segmentAssets.Assets.GetAssetsByVersion(segmentAssets.DefaultAssetVersion);
}
}
return combinedSegmentListAssets;
}
}
}

Просмотреть файл

@ -0,0 +1,52 @@
// -----------------------------------------------------------------------
// <copyright file="CoreSegment.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.Configuration.WebPortal
{
/// <summary>
/// A container for portal core asset segments.
/// </summary>
public class CoreSegment
{
/// <summary>
/// The start up assets segment.
/// </summary>
private AssetsSegment startup;
/// <summary>
/// The non start up assets segment.
/// </summary>
private AssetsSegment nonStartup;
/// <summary>
/// Gets or sets startup assets.
/// </summary>
public AssetsSegment Startup
{
get => this.startup;
set
{
this.startup = value;
this.startup.Name = "Startup";
}
}
/// <summary>
/// Gets or sets non startup assets.
/// </summary>
public AssetsSegment NonStartup
{
get => this.nonStartup;
set
{
this.nonStartup = value;
this.nonStartup.Name = "Nonstartup";
}
}
}
}

Просмотреть файл

@ -0,0 +1,142 @@
// -----------------------------------------------------------------------
// <copyright file="Plugin.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.Configuration.WebPortal
{
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
/// <summary>
/// Represents a Plugin in the web portal.
/// </summary>
[JsonObject]
public class Plugin : PluginDefaults, ICloneable
{
/// <summary>
/// Gets or sets the plugin Name.
/// </summary>
[JsonProperty]
public string Name { get; set; }
/// <summary>
/// Gets or sets the plugin's default feature.
/// </summary>
[JsonProperty]
public string DefaultFeature { get; set; }
/// <summary>
/// Gets or sets the plugin's features.
/// </summary>
[JsonProperty]
public IList<AssetsSegment> Features { get; set; }
/// <summary>
/// Sets the plugin default property values using the given defaults if these properties were missing.
/// </summary>
/// <param name="defaults">The plugin defaults.</param>
public void SetDefaults(PluginDefaults defaults)
{
if (defaults == null)
{
throw new ArgumentNullException(nameof(defaults), "defaults cannot be null");
}
defaults.Validate();
if (string.IsNullOrWhiteSpace(this.DisplayName))
{
this.DisplayName = defaults.DisplayName;
}
if (string.IsNullOrWhiteSpace(this.Image))
{
this.Image = defaults.Image;
}
if (string.IsNullOrWhiteSpace(this.Color))
{
this.Color = defaults.Color;
}
if (string.IsNullOrWhiteSpace(this.AlternateColor))
{
this.AlternateColor = defaults.AlternateColor;
}
}
/// <summary>
/// Validates the plugin's settings.
/// </summary>
/// <param name="featureHashtable">A feature hash table useful for cross referencing duplications in other plugins.</param>
/// <exception cref="InvalidOperationException">If the plugin settings are invalid.</exception>
public override void Validate(IDictionary<string, int> featureHashtable)
{
base.Validate();
if (string.IsNullOrWhiteSpace(this.Name))
{
throw new InvalidOperationException("Name not set");
}
if (this.Features == null || this.Features.Count <= 0)
{
throw new InvalidOperationException("Features not set");
}
if (string.IsNullOrWhiteSpace(this.DefaultFeature))
{
// if no default feature is set, set it to the first feature
this.DefaultFeature = this.Features[0].Name;
}
foreach (AssetsSegment feature in this.Features)
{
if (string.IsNullOrWhiteSpace(feature.Name))
{
throw new InvalidOperationException("A feature name can't be null or empty");
}
foreach (var featureAssets in feature.Assets)
{
featureAssets.Validate();
}
if (featureHashtable.ContainsKey(feature.Name))
{
throw new InvalidOperationException("Duplicate feature: " + feature.Name);
}
else
{
featureHashtable[feature.Name] = 0;
}
}
if (!featureHashtable.ContainsKey(this.DefaultFeature))
{
throw new InvalidOperationException("Invalid default feature. Please make sure it is addded to feature list");
}
}
/// <summary>
/// Clones the plugin instance.
/// </summary>
/// <returns>A deep copy of the plugin instance.</returns>
public new object Clone()
{
return new Plugin()
{
Name = this.Name,
Image = this.Image,
DisplayName = this.DisplayName,
Color = this.Color,
AlternateColor = this.AlternateColor,
DefaultFeature = this.DefaultFeature,
Features = this.Features.Clone()
};
}
}
}

Просмотреть файл

@ -0,0 +1,92 @@
// -----------------------------------------------------------------------
// <copyright file="PluginDefaults.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.Configuration.WebPortal
{
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
/// <summary>
/// Holds default property values for the web portal plugins.
/// </summary>
public class PluginDefaults : ICloneable
{
/// <summary>
/// Gets or sets the plugin display name.
/// </summary>
[JsonProperty]
public string DisplayName { get; set; }
/// <summary>
/// Gets or sets the plugin's tile icon.
/// </summary>
[JsonProperty("Tile")]
public string Image { get; set; }
/// <summary>
/// Gets or sets the plugin's theme color.
/// </summary>
[JsonProperty]
public string Color { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the plugin is hidden or not.
/// </summary>
[JsonProperty]
public bool Hidden { get; set; }
/// <summary>
/// Gets or sets the alternate color of the plugin's theme.
/// </summary>
[JsonProperty]
public string AlternateColor { get; set; }
/// <summary>
/// Checks if the plugin defaults are properly set or not.
/// </summary>
/// <param name="featureHashtable">A feature hash table useful for cross referencing duplications in other plugins.</param>
/// <exception cref="InvalidOperationException">If validation fails.</exception>
public virtual void Validate(IDictionary<string, int> featureHashtable = null)
{
if (string.IsNullOrWhiteSpace(this.DisplayName))
{
throw new InvalidOperationException("DisplayName not set");
}
if (string.IsNullOrWhiteSpace(this.Image))
{
throw new InvalidOperationException("Tile not set");
}
if (string.IsNullOrWhiteSpace(this.Color))
{
throw new InvalidOperationException("Color not set");
}
if (string.IsNullOrWhiteSpace(this.AlternateColor))
{
throw new InvalidOperationException("AlternateColor not set");
}
}
/// <summary>
/// Clones the plugin defaults object.
/// </summary>
/// <returns>A deep clone of the plugin defaults object.</returns>
public object Clone()
{
return new PluginDefaults()
{
AlternateColor = this.AlternateColor,
Color = this.Color,
DisplayName = this.DisplayName,
Hidden = this.Hidden,
Image = this.Image
};
}
}
}

Просмотреть файл

@ -0,0 +1,121 @@
//// -----------------------------------------------------------------------
//// <copyright file="PluginsSegment.cs" company="Microsoft">
//// Copyright (c) Microsoft Corporation. All rights reserved.
//// </copyright>
//// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.Configuration.WebPortal
{
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
/// <summary>
/// Handles the plugins configuration segment.
/// </summary>
public class PluginsSegment : ICloneable
{
/// <summary>
/// Gets or sets plugin defaults.
/// </summary>
[JsonProperty]
public PluginDefaults Defaults { get; set; }
/// <summary>
/// Gets or sets common plugin assets.
/// </summary>
[JsonProperty]
public AssetsSegment Commons { get; set; }
/// <summary>
/// Gets or sets supported plugins.
/// </summary>
[JsonProperty]
public IList<Plugin> Plugins { get; set; }
/// <summary>
/// Gets or sets the default plugin.
/// </summary>
[JsonProperty]
public string DefaultPlugin { get; set; }
/// <summary>
/// Validates the plugin configuration and ensures it is consistent and meaningful to the client.
/// </summary>
public void Validate()
{
if (this.Defaults == null)
{
throw new InvalidOperationException("Portal defaults not found");
}
this.Defaults.Validate();
if (this.Commons != null)
{
foreach (Assets commonAssets in this.Commons.Assets)
{
commonAssets.Validate();
}
}
if (this.Plugins == null || this.Plugins.Count <= 0)
{
throw new InvalidOperationException("Portal plugins not found. Please add plugins.");
}
// these dictionaries are used to detect plugin and feature duplications
IDictionary<string, int> pluginHashtable = new Dictionary<string, int>();
IDictionary<string, int> featureHashtable = new Dictionary<string, int>();
foreach (Plugin plugin in this.Plugins)
{
if (plugin == null)
{
throw new InvalidOperationException("Portal plugin cannot be null");
}
plugin.SetDefaults(this.Defaults);
plugin.Validate(featureHashtable);
if (pluginHashtable.ContainsKey(plugin.Name))
{
throw new InvalidOperationException("Duplicate plugin: " + plugin.Name);
}
else
{
pluginHashtable[plugin.Name] = 0;
}
}
if (string.IsNullOrWhiteSpace(this.DefaultPlugin))
{
this.DefaultPlugin = this.Plugins[0].Name;
}
else
{
if (!pluginHashtable.ContainsKey(this.DefaultPlugin))
{
throw new InvalidOperationException("default plugin not found: " + this.DefaultPlugin);
}
}
}
/// <summary>
/// Deep clones the plugins segment.
/// </summary>
/// <returns>A deep copy of the plugin segment object.</returns>
public object Clone()
{
PluginsSegment clone = new PluginsSegment();
clone.Commons = this.Commons.Clone() as AssetsSegment;
clone.DefaultPlugin = this.DefaultPlugin;
clone.Defaults = this.Defaults.Clone() as PluginDefaults;
clone.Plugins = new List<Plugin>(this.Plugins.Select<Plugin, Plugin>(plugin => plugin.Clone() as Plugin));
return clone;
}
}
}

Просмотреть файл

@ -0,0 +1,114 @@
// -----------------------------------------------------------------------
// <copyright file="WebPortalConfiguration.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
namespace Microsoft.Store.PartnerCenter.Storefront.Configuration.WebPortal
{
using System;
using System.Collections.Generic;
/// <summary>
/// Holds the Web portal configuration.
/// </summary>
public class WebPortalConfiguration
{
/// <summary>
/// The dependencies assets segment.
/// </summary>
private AssetsSegment dependencies;
/// <summary>
/// Initializes a new instance of the <see cref="WebPortalConfiguration"/> class.
/// </summary>
public WebPortalConfiguration()
{
Configuration = new Dictionary<string, dynamic>();
}
/// <summary>
/// Gets or sets the portal dependencies.
/// </summary>
public AssetsSegment Dependencies
{
get => this.dependencies;
set
{
this.dependencies = value;
this.dependencies.Name = "Dependencies";
}
}
/// <summary>
/// Gets or sets the portal core assets.
/// </summary>
public CoreSegment Core { get; set; }
/// <summary>
/// Gets or sets the portal services assets.
/// </summary>
public IEnumerable<AssetsSegment> Services { get; set; }
/// <summary>
/// Gets or sets the portal views assets.
/// </summary>
public IEnumerable<AssetsSegment> Views { get; set; }
/// <summary>
/// Gets or sets the portal plugins assets.
/// </summary>
public PluginsSegment Plugins { get; set; }
/// <summary>
/// Gets or sets the portal configuration.
/// </summary>
public Dictionary<string, dynamic> Configuration { get; }
/// <summary>
/// Processes the configuration and ensures it is valid.
/// </summary>
/// <exception cref="InvalidOperationException">If the configuration is invalid.</exception>
public void Process()
{
if (this.Dependencies != null)
{
this.Dependencies.Validate();
}
if (this.Core == null || (this.Core.Startup == null && this.Core.NonStartup == null))
{
throw new InvalidOperationException("Portal core not present.");
}
if (this.Core.Startup != null)
{
this.Core.Startup.Validate();
}
if (this.Core.NonStartup != null)
{
this.Core.NonStartup.Validate();
}
if (this.Services != null)
{
foreach (AssetsSegment service in this.Services)
{
service.Validate();
}
}
if (this.Views != null)
{
foreach (AssetsSegment view in this.Views)
{
view.Validate();
}
}
this.Plugins.Validate();
}
}
}

Просмотреть файл

@ -0,0 +1,750 @@
{
"Dependencies": {
"DefaultAssetVersion": "Debug",
"Assets": [
{
"Version": "Debug",
"JavaScript": [
"~/Scripts/WebPortal/Dependencies/appinsights.js",
"~/Scripts/WebPortal/Dependencies/jquery-1.11.2.js",
"~/Scripts/WebPortal/Dependencies/jquery-ui.js",
"~/Scripts/WebPortal/Dependencies/knockout-3.2.0.debug.js",
"~/Scripts/WebPortal/Dependencies/jquery.validate.js"
],
"Css": [
]
},
{
"Version": "Release",
"JavaScript": [
"~/Scripts/WebPortal/Dependencies/appinsights.js",
"~/Scripts/WebPortal/Dependencies/jquery-1.11.2.min.js",
"~/Scripts/WebPortal/Dependencies/jquery-ui.min.js",
"~/Scripts/WebPortal/Dependencies/knockout-3.2.0.js",
"~/Scripts/WebPortal/Dependencies/jquery.validate.js"
],
"Css": [
]
}
]
},
"Core": {
"Startup": {
"DefaultAssetVersion": "Standard",
"Assets": [
{
"Version": "Standard",
"JavaScript": [
"~/Scripts/WebPortal/Core/WebPortal.js",
"~/Scripts/WebPortal/Infrastructure/Helpers.js",
"~/Scripts/WebPortal/Infrastructure/KnockOutExtensions.js",
"~/Scripts/WebPortal/Infrastructure/Settings.js",
"~/Scripts/WebPortal/Infrastructure/Diagnostics.js",
"~/Scripts/WebPortal/Core/Shell.js",
"~/Scripts/WebPortal/Core/SplashScreen.js",
"~/Scripts/WebPortal/Core/EventSystem.js",
"~/Scripts/WebPortal/Utilities/RetryableServerCall.js",
"~/Scripts/WebPortal/Utilities/Animation.js"
],
"Css": [
"~/Content/Styles/WebPortal/WebPortalStyles.css",
"~/Content/Styles/WebPortal/StandardSplashScreen.css"
],
"Templates": [
"StandardSplashScreen"
]
}
]
},
"NonStartup": {
"DefaultAssetVersion": "Standard",
"Assets": [
{
"Version": "Standard",
"JavaScript": [
"~/Scripts/WebPortal/Core/SessionManager.js",
"~/Scripts/WebPortal/Core/Presenter.js",
"~/Scripts/WebPortal/Core/TemplatePresenter.js",
"~/Scripts/WebPortal/Core/View.js",
"~/Scripts/WebPortal/Core/ContentPanel.js",
"~/Scripts/WebPortal/Core/Journey.js",
"~/Scripts/WebPortal/Core/UrlManager.js",
"~/Scripts/WebPortal/Core/ServerCallManager.js",
"~/Scripts/WebPortal/Core/PortalService.js",
"~/Scripts/WebPortal/Utilities/AsyncOperationSerializer.js",
"~/Scripts/WebPortal/Utilities/Throttler.js",
"~/Scripts/WebPortal/Utilities/Toggler.js"
],
"Css": [
]
}
]
}
},
"Services": [
{
"Name": "Commons",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/WebPortal/Services/Button.js"
],
"Css": [
]
}
]
},
{
"Name": "Actions",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/WebPortal/Services/Actions/Action.js",
"~/Scripts/WebPortal/Services/Actions/ActionsManager.js",
"~/Scripts/WebPortal/Services/Actions/ActionsService.js"
],
"Css": [
"~/Content/Styles/WebPortal/Actions.css"
],
"Templates": [
"~/Views/Services/Actions.cshtml"
]
}
]
},
{
"Name": "Dialog",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/WebPortal/Services/Dialog/Dialog.js"
],
"Css": [
"~/Content/Styles/WebPortal/Dialog.css"
],
"Templates": [
"~/Views/Services/Dialog.cshtml"
]
}
]
},
{
"Name": "HeaderBar",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/WebPortal/Services/HeaderBar/HeaderBar.js",
"~/Scripts/WebPortal/Services/HeaderBar/HeaderBarSection.js",
"~/Scripts/WebPortal/Services/HeaderBar/TitleSection.js",
"~/Scripts/WebPortal/Services/HeaderBar/ActionsSection.js",
"~/Scripts/WebPortal/Services/HeaderBar/LinksSection.js",
"~/Scripts/WebPortal/Services/HeaderBar/NotificationsSection.js",
"~/Scripts/WebPortal/Services/HeaderBar/UserSection.js"
],
"Css": [
"~/Content/Styles/WebPortal/HeaderBar.css"
],
"Templates": [
"~/Views/Services/HeaderBar.cshtml"
]
}
]
},
{
"Name": "Notifications",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/WebPortal/Services/Notifications/Notification.js",
"~/Scripts/WebPortal/Services/Notifications/NotificationsManager.js"
],
"Css": [
"~/Content/Styles/WebPortal/Notifications.css"
],
"Templates": [
"~/Views/Services/Notifications.cshtml"
]
}
]
},
{
"Name": "Login",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/WebPortal/Services/Login/Login.js"
],
"Css": [
"~/Content/Styles/WebPortal/Login.css"
],
"Templates": [
"~/Views/Services/Login.cshtml"
]
}
]
},
{
"Name": "PrimaryNavigation",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/WebPortal/Services/PrimaryNavigation/PrimaryNavigation.js"
],
"Css": [
"~/Content/Styles/WebPortal/PrimaryNavigation.css"
],
"Templates": [
"~/Views/Services/PrimaryNavigation.cshtml"
]
}
]
},
{
"Name": "UserMenu",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/WebPortal/Services/UserMenu/UserMenuService.js"
],
"Css": [
]
}
]
}
],
"Views": [
{
"Name": "List",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/WebPortal/Views/List/List.js",
"~/Scripts/WebPortal/Views/List/Column.js",
"~/Scripts/WebPortal/Views/List/ListRenderer.js",
"~/Scripts/WebPortal/Views/List/IListEventListener.js",
"~/Scripts/WebPortal/Views/List/TablePageRenderer.js",
"~/Scripts/WebPortal/Views/List/InfiniteScrollingRenderer.js"
],
"Css": [
"~/Content/Styles/WebPortal/List.css"
],
"Templates": [
"~/Views/Controls/List.cshtml"
]
}
]
},
{
"Name": "AddSubscriptions",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/Views/AddSubscriptionsView.js"
],
"Css": [
],
"Templates": [
"~/Views/Controls/AddSubscriptions.cshtml"
]
}
]
},
{
"Name": "NewCustomerProfile",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/Views/NewCustomerProfileView.js"
],
"Css": [
],
"Templates": [
"~/Views/Controls/NewCustomerProfile.cshtml"
]
}
]
},
{
"Name": "OfferTile",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/Views/OfferTile.js"
],
"Css": [
"~/Content/Styles/Plugins/OfferTile.css"
],
"Templates": [
"~/Views/Controls/OfferTile.cshtml"
]
}
]
},
{
"Name": "OffersCatalog",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/Views/OffersCatalog.js"
],
"Css": [
"~/Content/Styles/Plugins/OffersCatalog.css"
],
"Templates": [
"~/Views/Controls/OffersCatalog.cshtml"
]
}
]
}
],
"Plugins": {
"Defaults": {
"DisplayName": "Unknown",
"Tile": "/Content/Images/Plugins/Tiles/home-tile.png",
"Color": "#3D3C3A",
"AlternateColor": "#4D4C4A"
},
"Commons": {
"Name": "Commons",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/ErrorCode.js",
"~/Scripts/Plugins/CommerceOperationType.js"
],
"Css": [
"~/Content/Styles/Plugins/Common.css",
"~/Content/Styles/Plugins/AddSubscriptions.css"
]
}
]
},
"Plugins": [
{
"Name": "Home",
"DisplayName": "HomePluginDisplayName",
"Tile": "/Content/Images/Plugins/Tiles/home-tile.png",
"DefaultFeature": "Home",
"Features": [
{
"Name": "Home",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/HomePagePresenter.js"
],
"Css": [
"~/Content/Styles/Plugins/HomePage.css"
]
}
]
},
{
"Name": "CustomerRegistration",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/CustomerRegistrationPresenter.js"
],
"Css": [
"~/Content/Styles/Plugins/CustomerRegistration.css"
]
}
]
},
{
"Name": "RegistrationConfirmation",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"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",
"DisplayName": "AdminConsoleDisplayName",
"Tile": "/Content/Images/Plugins/Tiles/dashboard-tile.png",
"Color": "#3090C7",
"AlternateColor": "#2080B7",
"DefaultFeature": "AdminConsole",
"Features": [
{
"Name": "AdminConsole",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/AdminConsolePresenter.js"
],
"Css": [
"~/Content/Styles/Plugins/AdminConsole.css"
]
}
]
}
]
},
{
"Name": "BrandingSetup",
"DisplayName": "BrandingDisplayName",
"Tile": "/Content/Images/Plugins/Tiles/branding-tile.png",
"Color": "#2874a6",
"AlternateColor": "#3884b6",
"DefaultFeature": "BrandingSetup",
"Features": [
{
"Name": "BrandingSetup",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/BrandingSetupPresenter.js"
],
"Css": [
"~/Content/Styles/Plugins/BrandingSetup.css"
]
}
]
}
]
},
{
"Name": "PartnerOffersSetup",
"DisplayName": "OffersDisplayName",
"Tile": "/Content/Images/Plugins/Tiles/offers-tile.png",
"Color": "#25383C",
"AlternateColor": "#35484C",
"DefaultFeature": "OfferList",
"Features": [
{
"Name": "OfferList",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/OfferListPresenter.js"
],
"Css": [
"~/Content/Styles/Plugins/OfferList.css"
]
}
]
},
{
"Name": "AddOrUpdateOffers",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/AddOrUpdateOfferPresenter.js"
],
"Css": [
"~/Content/Styles/Plugins/AddOrUpdateOffer.css"
]
}
]
}
]
},
{
"Name": "PaymentSetup",
"DisplayName": "PaymentDisplayName",
"Tile": "/Content/Images/Plugins/Tiles/payment-tile.png",
"Color": "#667C26",
"AlternateColor": "#768C36",
"DefaultFeature": "PaymentSetup",
"Features": [
{
"Name": "PaymentSetup",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/PaymentSetupPresenter.js"
],
"Css": [
"~/Content/Styles/Plugins/PaymentSetup.css"
]
}
]
}
]
},
{
"Name": "CustomerManagementSetup",
"DisplayName": "CustomerManagementDisplayName",
"Tile": "/Content/Images/Plugins/Tiles/account-tile.png",
"Color": "#34634C",
"AlternateColor": "#44735C",
"DefaultFeature": "CustomerManagementSetup",
"Features": [
{
"Name": "CustomerManagementSetup",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/CustomerManagementSetupPresenter.js"
],
"Css": [
"~/Content/Styles/Plugins/CustomerManagementSetup.css"
]
}
]
}
]
}, {
"Name": "CustomerAccount",
"DisplayName": "MyAccountDisplayName",
"Tile": "/Content/Images/Plugins/Tiles/account-tile.png",
"Color": "#667C26",
"AlternateColor": "#768C36",
"DefaultFeature": "CustomerAccount",
"Features": [
{
"Name": "CustomerAccount",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/CustomerAccountPresenter.js"
],
"Css": [
"~/Content/Styles/Plugins/CustomerAccount.css"
]
}
]
},
{
"Name": "UpdateContactInformation",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/UpdateContactInformationPresenter.js"
],
"Css": [
]
}
]
},
{
"Name": "UpdateCompanyInformation",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/UpdateCompanyInformationPresenter.js"
],
"Css": [
]
}
]
}
]
},
{
"Name": "CustomerSubscriptions",
"DisplayName": "MySubscriptionsDisplayName",
"Tile": "/Content/Images/Plugins/Tiles/subscriptions-tile.png",
"Color": "#3090C7",
"AlternateColor": "#2080B7",
"DefaultFeature": "Subscriptions",
"Features": [
{
"Name": "Subscriptions",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/SubscriptionsPresenter.js"
],
"Css": [
"~/Content/Styles/Plugins/Subscriptions.css"
]
}
]
},
{
"Name": "AddSubscriptions",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/AddSubscriptionsPresenter.js"
],
"Css": [
"~/Content/Styles/Plugins/AddSubscriptionsPage.css"
]
}
]
},
{
"Name": "UpdateSubscriptions",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/UpdateSubscriptionsPresenter.js"
],
"Css": [
"~/Content/Styles/Plugins/UpdateSubscriptions.css"
]
}
]
}
]
}
],
"DefaultPlugin": "Home"
},
"Configuration": {
/* The default animation duration used in the portal. */
"DefaultAnimationDuration": 300,
/* The default throttling duration in milleseconds used by the throttledChange KO binding. */
"DefaultThrottlingDuration": 450,
/* The minimum supported resolution. */
"MinimumResolution": {
"Width": 1024,
"Height": 600
},
"HeaderBar": {
"Height": 41
},
/* Primary navigation configuration. */
"PrimaryNavigation": {
"Template": "tileBasedPrimaryNavigation-template"
},
/* Action bar configuration. */
"ActionBar": {
"MenuBackgroundColor": "#333333",
"MenuHoverBackgroundColor": "#333333",
"MenuTextColor": "white",
"MenuHoverTextColor": "#007acc",
/* The maximum number of actions to display in the actions bar before adding the other actions to the extra (...) action as children. */
"MaxActions": 5
},
/* Control panel settings. */
"ControlPanel": {
"AnimationDuration": 650,
/* The width of the fully expanded control panel. */
"FullWidth": 191
},
"Notifications": {
"Template": "notificationPanel-template"
},
/* Server call timeout configuration. */
"Timeout": {
"Default": 60000,
"Min": 2000,
"Max": 180000
},
/* Diagnostics configuration. */
"Diagnostics": {
"Level": {
"Info": "INFO",
"Warning": "WARNING",
"Error": "ERROR"
}
},
/* Server Web API urls. Add your Web API urls here. */
"WebApi": {
"WebPortalContent": "/Template/FrameworkFragments",
"Expenses": {
"GetExpenses": "/api/expenses"
}
},
"List": {
"PageSize": 20,
"Sensitivity": 0.85
}
}
}

Просмотреть файл

@ -0,0 +1,750 @@
{
"Dependencies": {
"DefaultAssetVersion": "Debug",
"Assets": [
{
"Version": "Debug",
"JavaScript": [
"~/Scripts/WebPortal/Dependencies/appinsights.js",
"~/Scripts/WebPortal/Dependencies/jquery-1.11.2.js",
"~/Scripts/WebPortal/Dependencies/jquery-ui.js",
"~/Scripts/WebPortal/Dependencies/knockout-3.2.0.debug.js",
"~/Scripts/WebPortal/Dependencies/jquery.validate.js"
],
"Css": [
]
},
{
"Version": "Release",
"JavaScript": [
"~/Scripts/WebPortal/Dependencies/appinsights.js",
"~/Scripts/WebPortal/Dependencies/jquery-1.11.2.min.js",
"~/Scripts/WebPortal/Dependencies/jquery-ui.min.js",
"~/Scripts/WebPortal/Dependencies/knockout-3.2.0.js",
"~/Scripts/WebPortal/Dependencies/jquery.validate.js"
],
"Css": [
]
}
]
},
"Core": {
"Startup": {
"DefaultAssetVersion": "Standard",
"Assets": [
{
"Version": "Standard",
"JavaScript": [
"~/Scripts/WebPortal/Core/WebPortal.js",
"~/Scripts/WebPortal/Infrastructure/Helpers.js",
"~/Scripts/WebPortal/Infrastructure/KnockOutExtensions.js",
"~/Scripts/WebPortal/Infrastructure/Settings.js",
"~/Scripts/WebPortal/Infrastructure/Diagnostics.js",
"~/Scripts/WebPortal/Core/Shell.js",
"~/Scripts/WebPortal/Core/SplashScreen.js",
"~/Scripts/WebPortal/Core/EventSystem.js",
"~/Scripts/WebPortal/Utilities/RetryableServerCall.js",
"~/Scripts/WebPortal/Utilities/Animation.js"
],
"Css": [
"~/Content/Styles/WebPortal/WebPortalStyles.css",
"~/Content/Styles/WebPortal/StandardSplashScreen.css"
],
"Templates": [
"StandardSplashScreen"
]
}
]
},
"NonStartup": {
"DefaultAssetVersion": "Standard",
"Assets": [
{
"Version": "Standard",
"JavaScript": [
"~/Scripts/WebPortal/Core/SessionManager.js",
"~/Scripts/WebPortal/Core/Presenter.js",
"~/Scripts/WebPortal/Core/TemplatePresenter.js",
"~/Scripts/WebPortal/Core/View.js",
"~/Scripts/WebPortal/Core/ContentPanel.js",
"~/Scripts/WebPortal/Core/Journey.js",
"~/Scripts/WebPortal/Core/UrlManager.js",
"~/Scripts/WebPortal/Core/ServerCallManager.js",
"~/Scripts/WebPortal/Core/PortalService.js",
"~/Scripts/WebPortal/Utilities/AsyncOperationSerializer.js",
"~/Scripts/WebPortal/Utilities/Throttler.js",
"~/Scripts/WebPortal/Utilities/Toggler.js"
],
"Css": [
]
}
]
}
},
"Services": [
{
"Name": "Commons",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/WebPortal/Services/Button.js"
],
"Css": [
]
}
]
},
{
"Name": "Actions",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/WebPortal/Services/Actions/Action.js",
"~/Scripts/WebPortal/Services/Actions/ActionsManager.js",
"~/Scripts/WebPortal/Services/Actions/ActionsService.js"
],
"Css": [
"~/Content/Styles/WebPortal/Actions.css"
],
"Templates": [
"~/Views/Services/Actions.cshtml"
]
}
]
},
{
"Name": "Dialog",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/WebPortal/Services/Dialog/Dialog.js"
],
"Css": [
"~/Content/Styles/WebPortal/Dialog.css"
],
"Templates": [
"~/Views/Services/Dialog.cshtml"
]
}
]
},
{
"Name": "HeaderBar",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/WebPortal/Services/HeaderBar/HeaderBar.js",
"~/Scripts/WebPortal/Services/HeaderBar/HeaderBarSection.js",
"~/Scripts/WebPortal/Services/HeaderBar/TitleSection.js",
"~/Scripts/WebPortal/Services/HeaderBar/ActionsSection.js",
"~/Scripts/WebPortal/Services/HeaderBar/LinksSection.js",
"~/Scripts/WebPortal/Services/HeaderBar/NotificationsSection.js",
"~/Scripts/WebPortal/Services/HeaderBar/UserSection.js"
],
"Css": [
"~/Content/Styles/WebPortal/HeaderBar.css"
],
"Templates": [
"~/Views/Services/HeaderBar.cshtml"
]
}
]
},
{
"Name": "Notifications",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/WebPortal/Services/Notifications/Notification.js",
"~/Scripts/WebPortal/Services/Notifications/NotificationsManager.js"
],
"Css": [
"~/Content/Styles/WebPortal/Notifications.css"
],
"Templates": [
"~/Views/Services/Notifications.cshtml"
]
}
]
},
{
"Name": "Login",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/WebPortal/Services/Login/Login.js"
],
"Css": [
"~/Content/Styles/WebPortal/Login.css"
],
"Templates": [
"~/Views/Services/Login.cshtml"
]
}
]
},
{
"Name": "PrimaryNavigation",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/WebPortal/Services/PrimaryNavigation/PrimaryNavigation.js"
],
"Css": [
"~/Content/Styles/WebPortal/PrimaryNavigation.css"
],
"Templates": [
"~/Views/Services/PrimaryNavigation.cshtml"
]
}
]
},
{
"Name": "UserMenu",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/WebPortal/Services/UserMenu/UserMenuService.js"
],
"Css": [
]
}
]
}
],
"Views": [
{
"Name": "List",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/WebPortal/Views/List/List.js",
"~/Scripts/WebPortal/Views/List/Column.js",
"~/Scripts/WebPortal/Views/List/ListRenderer.js",
"~/Scripts/WebPortal/Views/List/IListEventListener.js",
"~/Scripts/WebPortal/Views/List/TablePageRenderer.js",
"~/Scripts/WebPortal/Views/List/InfiniteScrollingRenderer.js"
],
"Css": [
"~/Content/Styles/WebPortal/List.css"
],
"Templates": [
"~/Views/Controls/List.cshtml"
]
}
]
},
{
"Name": "AddSubscriptions",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/Views/AddSubscriptionsView.js"
],
"Css": [
],
"Templates": [
"~/Views/Controls/AddSubscriptions.cshtml"
]
}
]
},
{
"Name": "NewCustomerProfile",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/Views/NewCustomerProfileView.js"
],
"Css": [
],
"Templates": [
"~/Views/Controls/NewCustomerProfile.cshtml"
]
}
]
},
{
"Name": "OfferTile",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/Views/OfferTile.js"
],
"Css": [
"~/Content/Styles/Plugins/OfferTile.css"
],
"Templates": [
"~/Views/Controls/OfferTile.cshtml"
]
}
]
},
{
"Name": "OffersCatalog",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/Views/OffersCatalog.js"
],
"Css": [
"~/Content/Styles/Plugins/OffersCatalog.css"
],
"Templates": [
"~/Views/Controls/OffersCatalog.cshtml"
]
}
]
}
],
"Plugins": {
"Defaults": {
"DisplayName": "Unknown",
"Tile": "/Content/Images/Plugins/Tiles/home-tile.png",
"Color": "#3D3C3A",
"AlternateColor": "#4D4C4A"
},
"Commons": {
"Name": "Commons",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/ErrorCode.js",
"~/Scripts/Plugins/CommerceOperationType.js"
],
"Css": [
"~/Content/Styles/Plugins/Common.css",
"~/Content/Styles/Plugins/AddSubscriptions.css"
]
}
]
},
"Plugins": [
{
"Name": "Home",
"DisplayName": "HomePluginDisplayName",
"Tile": "/Content/Images/Plugins/Tiles/home-tile.png",
"DefaultFeature": "Home",
"Features": [
{
"Name": "Home",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/HomePagePresenter.js"
],
"Css": [
"~/Content/Styles/Plugins/HomePage.css"
]
}
]
},
{
"Name": "CustomerRegistration",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/CustomerRegistrationPresenterPayU.js"
],
"Css": [
"~/Content/Styles/Plugins/CustomerRegistration.css"
]
}
]
},
{
"Name": "RegistrationConfirmation",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"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/ProcessOrderPresenterPayU.js"
],
"Css": [
"~/Content/Styles/Plugins/ProcessOrder.css"
]
}
]
}
]
},
{
"Name": "AdminConsole",
"DisplayName": "AdminConsoleDisplayName",
"Tile": "/Content/Images/Plugins/Tiles/dashboard-tile.png",
"Color": "#3090C7",
"AlternateColor": "#2080B7",
"DefaultFeature": "AdminConsole",
"Features": [
{
"Name": "AdminConsole",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/AdminConsolePresenter.js"
],
"Css": [
"~/Content/Styles/Plugins/AdminConsole.css"
]
}
]
}
]
},
{
"Name": "BrandingSetup",
"DisplayName": "BrandingDisplayName",
"Tile": "/Content/Images/Plugins/Tiles/branding-tile.png",
"Color": "#2874a6",
"AlternateColor": "#3884b6",
"DefaultFeature": "BrandingSetup",
"Features": [
{
"Name": "BrandingSetup",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/BrandingSetupPresenter.js"
],
"Css": [
"~/Content/Styles/Plugins/BrandingSetup.css"
]
}
]
}
]
},
{
"Name": "PartnerOffersSetup",
"DisplayName": "OffersDisplayName",
"Tile": "/Content/Images/Plugins/Tiles/offers-tile.png",
"Color": "#25383C",
"AlternateColor": "#35484C",
"DefaultFeature": "OfferList",
"Features": [
{
"Name": "OfferList",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/OfferListPresenter.js"
],
"Css": [
"~/Content/Styles/Plugins/OfferList.css"
]
}
]
},
{
"Name": "AddOrUpdateOffers",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/AddOrUpdateOfferPresenter.js"
],
"Css": [
"~/Content/Styles/Plugins/AddOrUpdateOffer.css"
]
}
]
}
]
},
{
"Name": "PaymentSetup",
"DisplayName": "PaymentDisplayName",
"Tile": "/Content/Images/Plugins/Tiles/payment-tile.png",
"Color": "#667C26",
"AlternateColor": "#768C36",
"DefaultFeature": "PaymentSetup",
"Features": [
{
"Name": "PaymentSetup",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/PayUPaymentSetupPresenter.js"
],
"Css": [
"~/Content/Styles/Plugins/PaymentSetup.css"
]
}
]
}
]
},
{
"Name": "CustomerManagementSetup",
"DisplayName": "CustomerManagementDisplayName",
"Tile": "/Content/Images/Plugins/Tiles/account-tile.png",
"Color": "#34634C",
"AlternateColor": "#44735C",
"DefaultFeature": "CustomerManagementSetup",
"Features": [
{
"Name": "CustomerManagementSetup",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/CustomerManagementSetupPresenter.js"
],
"Css": [
"~/Content/Styles/Plugins/CustomerManagementSetup.css"
]
}
]
}
]
}, {
"Name": "CustomerAccount",
"DisplayName": "MyAccountDisplayName",
"Tile": "/Content/Images/Plugins/Tiles/account-tile.png",
"Color": "#667C26",
"AlternateColor": "#768C36",
"DefaultFeature": "CustomerAccount",
"Features": [
{
"Name": "CustomerAccount",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/CustomerAccountPresenter.js"
],
"Css": [
"~/Content/Styles/Plugins/CustomerAccount.css"
]
}
]
},
{
"Name": "UpdateContactInformation",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/UpdateContactInformationPresenter.js"
],
"Css": [
]
}
]
},
{
"Name": "UpdateCompanyInformation",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/UpdateCompanyInformationPresenter.js"
],
"Css": [
]
}
]
}
]
},
{
"Name": "CustomerSubscriptions",
"DisplayName": "MySubscriptionsDisplayName",
"Tile": "/Content/Images/Plugins/Tiles/subscriptions-tile.png",
"Color": "#3090C7",
"AlternateColor": "#2080B7",
"DefaultFeature": "Subscriptions",
"Features": [
{
"Name": "Subscriptions",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/SubscriptionsPresenter.js"
],
"Css": [
"~/Content/Styles/Plugins/Subscriptions.css"
]
}
]
},
{
"Name": "AddSubscriptions",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/AddSubscriptionsPresenterPayU.js"
],
"Css": [
"~/Content/Styles/Plugins/AddSubscriptionsPage.css"
]
}
]
},
{
"Name": "UpdateSubscriptions",
"DefaultAssetVersion": "1.0",
"Assets": [
{
"Version": "1.0",
"JavaScript": [
"~/Scripts/Plugins/UpdateSubscriptionsPresenterPayU.js"
],
"Css": [
"~/Content/Styles/Plugins/UpdateSubscriptions.css"
]
}
]
}
]
}
],
"DefaultPlugin": "Home"
},
"Configuration": {
/* The default animation duration used in the portal. */
"DefaultAnimationDuration": 300,
/* The default throttling duration in milleseconds used by the throttledChange KO binding. */
"DefaultThrottlingDuration": 450,
/* The minimum supported resolution. */
"MinimumResolution": {
"Width": 1024,
"Height": 600
},
"HeaderBar": {
"Height": 41
},
/* Primary navigation configuration. */
"PrimaryNavigation": {
"Template": "tileBasedPrimaryNavigation-template"
},
/* Action bar configuration. */
"ActionBar": {
"MenuBackgroundColor": "#333333",
"MenuHoverBackgroundColor": "#333333",
"MenuTextColor": "white",
"MenuHoverTextColor": "#007acc",
/* The maximum number of actions to display in the actions bar before adding the other actions to the extra (...) action as children. */
"MaxActions": 5
},
/* Control panel settings. */
"ControlPanel": {
"AnimationDuration": 650,
/* The width of the fully expanded control panel. */
"FullWidth": 191
},
"Notifications": {
"Template": "notificationPanel-template"
},
/* Server call timeout configuration. */
"Timeout": {
"Default": 60000,
"Min": 2000,
"Max": 180000
},
/* Diagnostics configuration. */
"Diagnostics": {
"Level": {
"Info": "INFO",
"Warning": "WARNING",
"Error": "ERROR"
}
},
/* Server Web API urls. Add your Web API urls here. */
"WebApi": {
"WebPortalContent": "/Template/FrameworkFragments",
"Expenses": {
"GetExpenses": "/api/expenses"
}
},
"List": {
"PageSize": 20,
"Sensitivity": 0.85
}
}
}

Двоичные данные
src/Storefront/Content/Images/Plugins/ProductLogos/azure-logo.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.2 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 5.1 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 4.3 KiB

Двоичные данные
src/Storefront/Content/Images/Plugins/ProductLogos/intune-logo.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 5.3 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.3 KiB

Двоичные данные
src/Storefront/Content/Images/Plugins/ProductLogos/office-logo.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 4.1 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 5.4 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.4 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 4.5 KiB

Двоичные данные
src/Storefront/Content/Images/Plugins/ProductLogos/skype-logo.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 5.3 KiB

Двоичные данные
src/Storefront/Content/Images/Plugins/ProductLogos/visio-logo.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.4 KiB

Двоичные данные
src/Storefront/Content/Images/Plugins/ProductLogos/yammer-logo.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 6.2 KiB

Двоичные данные
src/Storefront/Content/Images/Plugins/Tiles/account-tile.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 2.5 KiB

Двоичные данные
src/Storefront/Content/Images/Plugins/Tiles/branding-tile.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 2.3 KiB

Двоичные данные
src/Storefront/Content/Images/Plugins/Tiles/dashboard-tile.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 833 B

Двоичные данные
src/Storefront/Content/Images/Plugins/Tiles/home-tile.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.4 KiB

Двоичные данные
src/Storefront/Content/Images/Plugins/Tiles/offers-tile.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 2.4 KiB

Двоичные данные
src/Storefront/Content/Images/Plugins/Tiles/payment-tile.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.8 KiB

Двоичные данные
src/Storefront/Content/Images/Plugins/Tiles/subscriptions-tile.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.4 KiB

Двоичные данные
src/Storefront/Content/Images/Plugins/action-delete.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 928 B

Двоичные данные
src/Storefront/Content/Images/Plugins/action-new.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 800 B

Двоичные данные
src/Storefront/Content/Images/Plugins/action-pick.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 778 B

Двоичные данные
src/Storefront/Content/Images/Plugins/action-refresh.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 844 B

Двоичные данные
src/Storefront/Content/Images/Plugins/action-save.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 840 B

Двоичные данные
src/Storefront/Content/Images/Plugins/action-undo.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 881 B

Двоичные данные
src/Storefront/Content/Images/Plugins/banner.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 624 KiB

Двоичные данные
src/Storefront/Content/Images/Plugins/cloud.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.8 KiB

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше