Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 60 additions & 9 deletions src/Storefront/BusinessLogic/Commerce/CommerceOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,33 @@ public CommerceOperations(ApplicationDomain applicationDomain, string customerId
/// 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>
/// <param name="ratePerSeat">The subscription's price per seat, either yearly or mothly depending on the billing cycle.</param>
/// <param name="billingCycle">The billing cycle.</param>
/// <returns>The prorated amount to charge for the new extra seat.</returns>
public static decimal CalculateProratedSeatCharge(DateTime expiryDate, decimal yearlyRatePerSeat)
public static decimal CalculateProratedSeatCharge(DateTime expiryDate, decimal ratePerSeat, BillingCycleType billingCycle)
{
DateTime rightNow = DateTime.UtcNow;
expiryDate = expiryDate.ToUniversalTime();

// Determine the total yearly price per seat
decimal yearlyRatePerSeat;

switch (billingCycle)
{
case BillingCycleType.Annual:
yearlyRatePerSeat = ratePerSeat;
break;
case BillingCycleType.Monthly:
yearlyRatePerSeat = ratePerSeat * 12m;
break;
default:
throw new NotImplementedException($"Billing cycle {billingCycle} is not implemented");
}

// Calculate the daily price per seat
decimal dailyChargePerSeat = yearlyRatePerSeat / 365m;

// round up the remaining days in case there was a fraction and ensure it does not exceed 365 days
// 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);

Expand Down Expand Up @@ -94,8 +111,10 @@ public async Task<TransactionResult> PurchaseAsync(OrderViewModel order)
AuthorizePayment paymentAuthorization = new AuthorizePayment(PaymentGateway);
subTransactions.Add(paymentAuthorization);

BrandingConfiguration portalBranding = await ApplicationDomain.Instance.PortalBranding.RetrieveAsync().ConfigureAwait(false);

// build the Partner Center order and pass it to the place order transaction
Order partnerCenterPurchaseOrder = BuildPartnerCenterOrder(lineItemsWithOffers);
Order partnerCenterPurchaseOrder = BuildPartnerCenterOrder(lineItemsWithOffers, portalBranding.BillingCycle);

PlaceOrder placeOrder = new PlaceOrder(
ApplicationDomain.PartnerCenterClient.Customers.ById(CustomerId),
Expand Down Expand Up @@ -217,10 +236,25 @@ public async Task<TransactionResult> RenewSubscriptionAsync(OrderViewModel order
ApplicationDomain.CustomerPurchasesRepository,
new CustomerPurchaseEntity(CommerceOperationType.Renewal, Guid.NewGuid().ToString(), CustomerId, subscriptionId, partnerCenterSubscription.Quantity, partnerOfferPrice, rightNow)));

// extend the expiry date by one year
DateTime expirationDate = subscriptionExpiryDate;
BrandingConfiguration portalBranding = await ApplicationDomain.Instance.PortalBranding.RetrieveAsync().ConfigureAwait(false);

switch (portalBranding.BillingCycle)
{
case BillingCycleType.Annual:
expirationDate = expirationDate.AddYears(1);
break;
case BillingCycleType.Monthly:
expirationDate = expirationDate.AddMonths(1);
break;
default:
throw new NotImplementedException($"Billing cycle {portalBranding.BillingCycle} is not implemented");
}

// extend the expiry date by one month or one year
subTransactions.Add(new UpdatePersistedSubscription(
ApplicationDomain.CustomerSubscriptionsRepository,
new CustomerSubscriptionEntity(CustomerId, subscriptionId, partnerOfferId, subscriptionExpiryDate.AddYears(1))));
new CustomerSubscriptionEntity(CustomerId, subscriptionId, partnerOfferId, expirationDate)));

// add a capture payment to the transaction pipeline
subTransactions.Add(new CapturePayment(PaymentGateway, () => paymentAuthorization.Result));
Expand Down Expand Up @@ -302,13 +336,14 @@ private async Task<IEnumerable<PurchaseLineItemWithOffer>> AssociateWithPartnerO
/// Builds a Microsoft Partner Center order from a list of purchase line items.
/// </summary>
/// <param name="purchaseLineItems">The purchase line items.</param>
/// <param name="billingCycle">The order billing cycle.</param>
/// <returns>The Partner Center Order.</returns>
private Order BuildPartnerCenterOrder(IEnumerable<PurchaseLineItemWithOffer> purchaseLineItems)
private Order BuildPartnerCenterOrder(IEnumerable<PurchaseLineItemWithOffer> purchaseLineItems, BillingCycleType billingCycle)
{
int lineItemNumber = 0;
ICollection<OrderLineItem> partnerCenterOrderLineItems = new List<OrderLineItem>();

// build the Partner Center order line items
// Build the Partner Center order line items
foreach (PurchaseLineItemWithOffer lineItem in purchaseLineItems)
{
// add the line items to the partner center order and calculate the price to charge
Expand All @@ -320,10 +355,26 @@ private Order BuildPartnerCenterOrder(IEnumerable<PurchaseLineItemWithOffer> pur
});
}

// bundle the order line items into a partner center order
// Get the store billing cycle and update the order
PartnerCenter.Models.Offers.BillingCycleType orderBillingCycle;

switch (billingCycle)
{
case BillingCycleType.Annual:
orderBillingCycle = PartnerCenter.Models.Offers.BillingCycleType.Annual;
break;
case BillingCycleType.Monthly:
orderBillingCycle = PartnerCenter.Models.Offers.BillingCycleType.Monthly;
break;
default:
throw new NotImplementedException($"Billing cycle {billingCycle} is not implemented");
}

// Bundle the order line items into a partner center order
return new Order()
{
ReferenceCustomerId = CustomerId,
BillingCycle = orderBillingCycle,
LineItems = partnerCenterOrderLineItems
};
}
Expand Down
9 changes: 7 additions & 2 deletions src/Storefront/BusinessLogic/Commerce/OrderNormalizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,13 @@ public async Task<OrderViewModel> NormalizePurchaseAdditionalSeatsOrderAsync()
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);
BrandingConfiguration portalBranding = await ApplicationDomain.Instance.PortalBranding.RetrieveAsync().ConfigureAwait(false);

decimal proratedSeatCharge = Math.Round(CommerceOperations.CalculateProratedSeatCharge(subscriptionToAugment.ExpiryDate,
partnerOffer.Price,
portalBranding.BillingCycle),
Resources.Culture.NumberFormat.CurrencyDecimalDigits);
//decimal totalCharge = Math.Round(proratedSeatCharge * seatsToPurchase, Resources.Culture.NumberFormat.CurrencyDecimalDigits);

List<OrderSubscriptionItemViewModel> resultOrderSubscriptions = new List<OrderSubscriptionItemViewModel>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,21 @@ public async Task ExecuteAsync()
ICollection<IBusinessTransaction> persistenceTransactions = new List<IBusinessTransaction>();

DateTime rightNow = DateTime.UtcNow;
DateTime expirationDate = rightNow;

BrandingConfiguration portalBranding = await ApplicationDomain.Instance.PortalBranding.RetrieveAsync().ConfigureAwait(false);

switch (portalBranding.BillingCycle)
{
case BillingCycleType.Annual:
expirationDate = expirationDate.AddYears(1);
break;
case BillingCycleType.Monthly:
expirationDate = expirationDate.AddMonths(1);
break;
default:
throw new NotImplementedException($"Billing cycle {portalBranding.BillingCycle} is not implemented");
}

foreach (OrderLineItem orderLineItem in partnerCenterPurchaseOrder.LineItems)
{
Expand All @@ -98,7 +113,7 @@ public async Task ExecuteAsync()
// add a record new customer subscription transaction for the current line item
persistenceTransactions.Add(new RecordNewCustomerSubscription(
CustomerSubscriptionsRepository,
new CustomerSubscriptionEntity(CustomerId, orderLineItem.SubscriptionId, matchingPartnerOffer.Id, rightNow.AddYears(1))));
new CustomerSubscriptionEntity(CustomerId, orderLineItem.SubscriptionId, matchingPartnerOffer.Id, expirationDate)));

// add a record purchase history for the current line item
persistenceTransactions.Add(new RecordPurchase(
Expand Down
20 changes: 19 additions & 1 deletion src/Storefront/BusinessLogic/Offers/PartnerOffersRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,28 @@ public async Task<IEnumerable<MicrosoftOffer>> RetrieveMicrosoftOffersAsync()
// Offers.ByCountry is required to pull country / region specific offers.
PartnerCenter.Models.ResourceCollection<PartnerCenter.Models.Offers.Offer> partnerCenterOffers = await localeSpecificPartnerCenterClient.Offers.ByCountry(ApplicationDomain.PortalLocalization.CountryIso2Code).GetAsync().ConfigureAwait(false);

// Get the current billing cycle to filter down the eligible offers
BrandingConfiguration portalBranding = await ApplicationDomain.Instance.PortalBranding.RetrieveAsync().ConfigureAwait(false);
PartnerCenter.Models.Offers.BillingCycleType offerBillingCycle = PartnerCenter.Models.Offers.BillingCycleType.Unknown;

switch (portalBranding.BillingCycle)
{
case BillingCycleType.Annual:
offerBillingCycle = PartnerCenter.Models.Offers.BillingCycleType.Monthly;
break;
case BillingCycleType.Monthly:
offerBillingCycle = PartnerCenter.Models.Offers.BillingCycleType.Annual;
break;
default:
throw new NotImplementedException($"Billing cycle {portalBranding.BillingCycle} is not implemented");
}

// Get offers which support the desired billing cycle
IEnumerable<PartnerCenter.Models.Offers.Offer> eligibleOffers = partnerCenterOffers?.Items.Where(offer =>
!offer.IsAddOn &&
(offer.PrerequisiteOffers == null || !offer.PrerequisiteOffers.Any())
&& offer.IsAvailableForPurchase);
&& offer.IsAvailableForPurchase
&& offer.SupportedBillingCycles.Contains(offerBillingCycle));

microsoftOffers = new List<MicrosoftOffer>();

Expand Down
8 changes: 7 additions & 1 deletion src/Storefront/Controllers/AdminConsoleController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@ public async Task<BrandingConfiguration> UpdateBrandingConfiguration()
brandingConfiguration.InstrumentationKey = HttpContext.Current.Request.Form["InstrumentationKey"];
}

if (!string.IsNullOrWhiteSpace(HttpContext.Current.Request.Form["BillingCycle"])
&& Enum.TryParse(HttpContext.Current.Request.Form["BillingCycle"], out BillingCycleType billingCycle))
{
brandingConfiguration.BillingCycle = billingCycle;
}

BrandingConfiguration updatedBrandingConfiguration = await ApplicationDomain.Instance.PortalBranding.UpdateAsync(brandingConfiguration).ConfigureAwait(false);
bool isPaymentConfigurationSetup = await ApplicationDomain.Instance.PaymentConfigurationRepository.IsConfiguredAsync().ConfigureAwait(false);

Expand All @@ -170,7 +176,7 @@ public async Task<BrandingConfiguration> UpdateBrandingConfiguration()
[HttpGet]
public async Task<IEnumerable<PartnerOffer>> GetOffers()
{
return (await ApplicationDomain.Instance.OffersRepository.RetrieveAsync().ConfigureAwait(false)).Where(offer => !offer.IsInactive);
return (await ApplicationDomain.Instance.OffersRepository.RetrieveAsync().ConfigureAwait(false)).OrderBy(o => o.DisplayIndex).Where(offer => !offer.IsInactive);
}

/// <summary>
Expand Down
9 changes: 7 additions & 2 deletions src/Storefront/Controllers/CustomerAccountController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,13 @@ private async Task<ManagedSubscriptionsViewModel> GetManagedSubscriptions()
isEditable = false;
}

// Compute the pro rated price per seat for this subcription & return for client side processing during updates.
decimal proratedPerSeatPrice = Math.Round(CommerceOperations.CalculateProratedSeatCharge(subscription.ExpiryDate, portalOfferPrice), responseCulture.NumberFormat.CurrencyDecimalDigits);
BrandingConfiguration portalBranding = await ApplicationDomain.Instance.PortalBranding.RetrieveAsync().ConfigureAwait(false);

// Compute the pro rated price per seat for this subcription & return for client side processing during updates.
decimal proratedPerSeatPrice = Math.Round(CommerceOperations.CalculateProratedSeatCharge(subscription.ExpiryDate,
portalOfferPrice,
portalBranding.BillingCycle),
Resources.Culture.NumberFormat.CurrencyDecimalDigits);

SubscriptionViewModel subscriptionItem = new SubscriptionViewModel()
{
Expand Down
7 changes: 6 additions & 1 deletion src/Storefront/Controllers/OrderController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -399,8 +399,13 @@ private async Task<SubscriptionsSummary> GetSubscriptionSummaryAsync(string cust
isEditable = false;
}

BrandingConfiguration portalBranding = await ApplicationDomain.Instance.PortalBranding.RetrieveAsync().ConfigureAwait(false);

// Compute the pro rated price per seat for this subcription & return for client side processing during updates.
decimal proratedPerSeatPrice = Math.Round(CommerceOperations.CalculateProratedSeatCharge(subscription.ExpiryDate, portalOfferPrice), responseCulture.NumberFormat.CurrencyDecimalDigits);
decimal proratedPerSeatPrice = Math.Round(CommerceOperations.CalculateProratedSeatCharge(subscription.ExpiryDate,
portalOfferPrice,
portalBranding.BillingCycle),
Resources.Culture.NumberFormat.CurrencyDecimalDigits);

SubscriptionViewModel subscriptionItem = new SubscriptionViewModel()
{
Expand Down
2 changes: 1 addition & 1 deletion src/Storefront/Controllers/PartnerOfferController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public async Task<OfferCatalogViewModel> GetOffersCatalog()
}
}

offerCatalogViewModel.Offers = partnerOffers.Where(offer => !offer.IsInactive);
offerCatalogViewModel.Offers = partnerOffers.OrderBy(o => o.DisplayIndex).Where(offer => !offer.IsInactive);
}

return offerCatalogViewModel;
Expand Down
2 changes: 2 additions & 0 deletions src/Storefront/Controllers/TemplateController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public async Task<ActionResult> Home()

ViewBag.OrganizationName = portalBranding.OrganizationName;
ViewBag.IsPortalAdmin = principal.IsPortalAdmin;
ViewBag.BillingCycle = portalBranding.BillingCycle;

return PartialView();
}
Expand Down Expand Up @@ -264,6 +265,7 @@ public async Task<ActionResult> FrameworkFragments()
ViewBag.ContactSales = portalBranding.ContactSales;

ViewBag.CurrencySymbol = ApplicationDomain.Instance.PortalLocalization.CurrencySymbol;
ViewBag.BillingCycle = portalBranding.BillingCycle;

return PartialView();
}
Expand Down
14 changes: 14 additions & 0 deletions src/Storefront/Models/BillingCycleType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// -----------------------------------------------------------------------
// <copyright file="BrandingConfiguration.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------

namespace Microsoft.Store.PartnerCenter.Storefront.Models
{
public enum BillingCycleType
{
Annual = 0,
Monthly = 1
}
}
5 changes: 5 additions & 0 deletions src/Storefront/Models/BrandingConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,10 @@ public class BrandingConfiguration
/// Gets or sets a privacy link for using the portal.
/// </summary>
public Uri PrivacyAgreement { get; set; }

/// <summary>
/// Gets or sets the configured billing cycle for the portal.
/// </summary>
public BillingCycleType BillingCycle { get; set; }
}
}
5 changes: 5 additions & 0 deletions src/Storefront/Models/PartnerOffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,10 @@ public class PartnerOffer
/// Gets or sets the offer's thumbnail image.
/// </summary>
public Uri Thumbnail { get; set; }

/// <summary>
/// Gets or sets the offer's display index.
/// </summary>
public int DisplayIndex { get; set; }
}
}
Loading