diff --git a/src/Storefront/BusinessLogic/Commerce/CommerceOperations.cs b/src/Storefront/BusinessLogic/Commerce/CommerceOperations.cs index 4578eb7..bd9ac67 100644 --- a/src/Storefront/BusinessLogic/Commerce/CommerceOperations.cs +++ b/src/Storefront/BusinessLogic/Commerce/CommerceOperations.cs @@ -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. /// /// The subscription's expiry date. - /// The subscription's yearly price per seat. + /// The subscription's price per seat, either yearly or mothly depending on the billing cycle. + /// The billing cycle. /// The prorated amount to charge for the new extra seat. - 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); @@ -94,8 +111,10 @@ public async Task 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), @@ -217,10 +236,25 @@ public async Task 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)); @@ -302,13 +336,14 @@ private async Task> AssociateWithPartnerO /// Builds a Microsoft Partner Center order from a list of purchase line items. /// /// The purchase line items. + /// The order billing cycle. /// The Partner Center Order. - private Order BuildPartnerCenterOrder(IEnumerable purchaseLineItems) + private Order BuildPartnerCenterOrder(IEnumerable purchaseLineItems, BillingCycleType billingCycle) { int lineItemNumber = 0; ICollection partnerCenterOrderLineItems = new List(); - // 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 @@ -320,10 +355,26 @@ private Order BuildPartnerCenterOrder(IEnumerable 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 }; } diff --git a/src/Storefront/BusinessLogic/Commerce/OrderNormalizer.cs b/src/Storefront/BusinessLogic/Commerce/OrderNormalizer.cs index 77eef47..502d4ef 100644 --- a/src/Storefront/BusinessLogic/Commerce/OrderNormalizer.cs +++ b/src/Storefront/BusinessLogic/Commerce/OrderNormalizer.cs @@ -222,8 +222,13 @@ public async Task 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 resultOrderSubscriptions = new List { diff --git a/src/Storefront/BusinessLogic/Commerce/Transactions/PersistNewlyPurchasedSubscriptions.cs b/src/Storefront/BusinessLogic/Commerce/Transactions/PersistNewlyPurchasedSubscriptions.cs index 112224f..48a2d7c 100644 --- a/src/Storefront/BusinessLogic/Commerce/Transactions/PersistNewlyPurchasedSubscriptions.cs +++ b/src/Storefront/BusinessLogic/Commerce/Transactions/PersistNewlyPurchasedSubscriptions.cs @@ -90,6 +90,21 @@ public async Task ExecuteAsync() ICollection persistenceTransactions = new List(); 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) { @@ -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( diff --git a/src/Storefront/BusinessLogic/Offers/PartnerOffersRepository.cs b/src/Storefront/BusinessLogic/Offers/PartnerOffersRepository.cs index 94eaacc..0f03334 100644 --- a/src/Storefront/BusinessLogic/Offers/PartnerOffersRepository.cs +++ b/src/Storefront/BusinessLogic/Offers/PartnerOffersRepository.cs @@ -71,10 +71,28 @@ public async Task> RetrieveMicrosoftOffersAsync() // Offers.ByCountry is required to pull country / region specific offers. PartnerCenter.Models.ResourceCollection 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 eligibleOffers = partnerCenterOffers?.Items.Where(offer => !offer.IsAddOn && (offer.PrerequisiteOffers == null || !offer.PrerequisiteOffers.Any()) - && offer.IsAvailableForPurchase); + && offer.IsAvailableForPurchase + && offer.SupportedBillingCycles.Contains(offerBillingCycle)); microsoftOffers = new List(); diff --git a/src/Storefront/Controllers/AdminConsoleController.cs b/src/Storefront/Controllers/AdminConsoleController.cs index a263432..3d57ee9 100644 --- a/src/Storefront/Controllers/AdminConsoleController.cs +++ b/src/Storefront/Controllers/AdminConsoleController.cs @@ -148,6 +148,12 @@ public async Task 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); @@ -170,7 +176,7 @@ public async Task UpdateBrandingConfiguration() [HttpGet] public async Task> 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); } /// diff --git a/src/Storefront/Controllers/CustomerAccountController.cs b/src/Storefront/Controllers/CustomerAccountController.cs index 35825d5..cfad78f 100644 --- a/src/Storefront/Controllers/CustomerAccountController.cs +++ b/src/Storefront/Controllers/CustomerAccountController.cs @@ -152,8 +152,13 @@ private async Task 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() { diff --git a/src/Storefront/Controllers/OrderController.cs b/src/Storefront/Controllers/OrderController.cs index 67af380..059f952 100644 --- a/src/Storefront/Controllers/OrderController.cs +++ b/src/Storefront/Controllers/OrderController.cs @@ -399,8 +399,13 @@ private async Task 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() { diff --git a/src/Storefront/Controllers/PartnerOfferController.cs b/src/Storefront/Controllers/PartnerOfferController.cs index 14ad74e..0e36a7d 100644 --- a/src/Storefront/Controllers/PartnerOfferController.cs +++ b/src/Storefront/Controllers/PartnerOfferController.cs @@ -59,7 +59,7 @@ public async Task GetOffersCatalog() } } - offerCatalogViewModel.Offers = partnerOffers.Where(offer => !offer.IsInactive); + offerCatalogViewModel.Offers = partnerOffers.OrderBy(o => o.DisplayIndex).Where(offer => !offer.IsInactive); } return offerCatalogViewModel; diff --git a/src/Storefront/Controllers/TemplateController.cs b/src/Storefront/Controllers/TemplateController.cs index 8188afa..4bbedc6 100644 --- a/src/Storefront/Controllers/TemplateController.cs +++ b/src/Storefront/Controllers/TemplateController.cs @@ -55,6 +55,7 @@ public async Task Home() ViewBag.OrganizationName = portalBranding.OrganizationName; ViewBag.IsPortalAdmin = principal.IsPortalAdmin; + ViewBag.BillingCycle = portalBranding.BillingCycle; return PartialView(); } @@ -264,6 +265,7 @@ public async Task FrameworkFragments() ViewBag.ContactSales = portalBranding.ContactSales; ViewBag.CurrencySymbol = ApplicationDomain.Instance.PortalLocalization.CurrencySymbol; + ViewBag.BillingCycle = portalBranding.BillingCycle; return PartialView(); } diff --git a/src/Storefront/Models/BillingCycleType.cs b/src/Storefront/Models/BillingCycleType.cs new file mode 100644 index 0000000..9cdb7c9 --- /dev/null +++ b/src/Storefront/Models/BillingCycleType.cs @@ -0,0 +1,14 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// ----------------------------------------------------------------------- + +namespace Microsoft.Store.PartnerCenter.Storefront.Models +{ + public enum BillingCycleType + { + Annual = 0, + Monthly = 1 + } +} \ No newline at end of file diff --git a/src/Storefront/Models/BrandingConfiguration.cs b/src/Storefront/Models/BrandingConfiguration.cs index 8daa143..b90b957 100644 --- a/src/Storefront/Models/BrandingConfiguration.cs +++ b/src/Storefront/Models/BrandingConfiguration.cs @@ -63,5 +63,10 @@ public class BrandingConfiguration /// Gets or sets a privacy link for using the portal. /// public Uri PrivacyAgreement { get; set; } + + /// + /// Gets or sets the configured billing cycle for the portal. + /// + public BillingCycleType BillingCycle { get; set; } } } \ No newline at end of file diff --git a/src/Storefront/Models/PartnerOffer.cs b/src/Storefront/Models/PartnerOffer.cs index ca809ae..dfa1612 100644 --- a/src/Storefront/Models/PartnerOffer.cs +++ b/src/Storefront/Models/PartnerOffer.cs @@ -58,5 +58,10 @@ public class PartnerOffer /// Gets or sets the offer's thumbnail image. /// public Uri Thumbnail { get; set; } + + /// + /// Gets or sets the offer's display index. + /// + public int DisplayIndex { get; set; } } } \ No newline at end of file diff --git a/src/Storefront/Resources.Designer.cs b/src/Storefront/Resources.Designer.cs index 971442a..fe7452f 100644 --- a/src/Storefront/Resources.Designer.cs +++ b/src/Storefront/Resources.Designer.cs @@ -294,6 +294,24 @@ public static string BadInputGenericMessage { } } + /// + /// Looks up a localized string similar to Billing cycle:. + /// + public static string BillingCycleFieldHeader { + get { + return ResourceManager.GetString("BillingCycleFieldHeader", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You can choose between Monthly or Annual billing for offers. Note that changing the billing cycle will not update pricing for any existing offers.. + /// + public static string BillingCycleFieldSubText { + get { + return ResourceManager.GetString("BillingCycleFieldSubText", resourceCulture); + } + } + /// /// Looks up a localized string similar to based billing. /// @@ -3345,6 +3363,15 @@ public static string Description { } } + /// + /// Looks up a localized string similar to Display index:. + /// + public static string DisplayIndexHeader { + get { + return ResourceManager.GetString("DisplayIndexHeader", resourceCulture); + } + } + /// /// Looks up a localized string similar to The domain is not available. Please enter another domain prefix. /// @@ -3948,6 +3975,24 @@ public static string MicrosoftOfferRetrievalErrorMessage { } } + /// + /// Looks up a localized string similar to Monthly price. + /// + public static string MonthlyPriceCaption { + get { + return ResourceManager.GetString("MonthlyPriceCaption", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Monthly total. + /// + public static string MonthlyTotalCaption { + get { + return ResourceManager.GetString("MonthlyTotalCaption", resourceCulture); + } + } + /// /// Looks up a localized string similar to More actions.... /// @@ -5136,6 +5181,24 @@ public static string SubscriptionSummaryAnnualPricePrefixCaption { } } + /// + /// Looks up a localized string similar to expires on. + /// + public static string SubscriptionSummaryMonthlyCommitmentExpiryCaption { + get { + return ResourceManager.GetString("SubscriptionSummaryMonthlyCommitmentExpiryCaption", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Price Paid. + /// + public static string SubscriptionSummaryMonthlyPricePrefixCaption { + get { + return ResourceManager.GetString("SubscriptionSummaryMonthlyPricePrefixCaption", resourceCulture); + } + } + /// /// Looks up a localized string similar to Price paid is displayed pro-rated as applicable. . /// @@ -5361,6 +5424,15 @@ public static string UserNameLabel { } } + /// + /// Looks up a localized string similar to user/month. + /// + public static string UserPerMonth { + get { + return ResourceManager.GetString("UserPerMonth", resourceCulture); + } + } + /// /// Looks up a localized string similar to user/year. /// @@ -5415,6 +5487,24 @@ public static string YourAnnualPriceCaption { } } + /// + /// Looks up a localized string similar to Display index:. + /// + public static string YourDisplayIndexCaption { + get { + return ResourceManager.GetString("YourDisplayIndexCaption", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Monthly price:. + /// + public static string YourMonthlyPriceCaption { + get { + return ResourceManager.GetString("YourMonthlyPriceCaption", resourceCulture); + } + } + /// /// Looks up a localized string similar to Offer subtitle:. /// diff --git a/src/Storefront/Resources.resx b/src/Storefront/Resources.resx index 72cba00..e9af972 100644 --- a/src/Storefront/Resources.resx +++ b/src/Storefront/Resources.resx @@ -1908,4 +1908,34 @@ Zimbabwe + + user/month + + + Billing cycle: + + + You can choose between Monthly or Annual billing for offers. Note that changing the billing cycle will not update pricing for any existing offers. + + + Display index: + + + Monthly price + + + Monthly total + + + expires on + + + Price Paid + + + Display index: + + + Monthly price: + \ No newline at end of file diff --git a/src/Storefront/Scripts/Plugins/AddOrUpdateOfferPresenter.js b/src/Storefront/Scripts/Plugins/AddOrUpdateOfferPresenter.js index 13d9bd1..5422714 100644 --- a/src/Storefront/Scripts/Plugins/AddOrUpdateOfferPresenter.js +++ b/src/Storefront/Scripts/Plugins/AddOrUpdateOfferPresenter.js @@ -18,6 +18,7 @@ MicrosoftOffer: ko.observable(this.isNewOffer ? "" : existingOffer.MicrosoftOffer), Title: ko.observable(this.isNewOffer ? "" : existingOffer.Title), SubTitle: ko.observable(this.isNewOffer ? "" : existingOffer.SubTitle), + DisplayIndex: ko.observable(this.isNewOffer ? "" : existingOffer.DisplayIndex), Price: ko.observable(this.isNewOffer ? "" : Globalize.format(existingOffer.Price, "n")), Features: ko.observableArray([]), Summary: ko.observableArray([]), @@ -203,7 +204,8 @@ Microsoft.WebPortal.AddOrUpdateOfferPresenter.prototype.onSaveOffer = function ( MicrosoftOfferId: this.viewModel.MicrosoftOffer().Offer.Id, Title: this.viewModel.Title(), Subtitle: this.viewModel.SubTitle(), - Price: 0.0 + Price: 0.0, + DisplayIndex: this.viewModel.DisplayIndex() }; // Only save the culture neutral value for price in the backend. @@ -283,6 +285,7 @@ Microsoft.WebPortal.AddOrUpdateOfferPresenter.prototype.onSelectBaseOffer = func self.viewModel.MicrosoftOffer(self.OfferSelectionWizardViewModel.offerList.getSelectedRows()[0].OriginalOffer); self.viewModel.Title(self.viewModel.MicrosoftOffer().Offer.Name); + self.viewModel.DisplayIndex(0); self.webPortal.Services.Dialog.hide(); self.saveOfferAction.enabled(true); }); diff --git a/src/Storefront/Scripts/Plugins/BrandingSetupPresenter.js b/src/Storefront/Scripts/Plugins/BrandingSetupPresenter.js index d7ac0ec..e699680 100644 --- a/src/Storefront/Scripts/Plugins/BrandingSetupPresenter.js +++ b/src/Storefront/Scripts/Plugins/BrandingSetupPresenter.js @@ -25,7 +25,9 @@ InstrumentationKey: ko.observable(""), OrganizationLogo: ko.observable(""), OrganizationName: ko.observable(""), - PrivacyAgreement: ko.observable("") + PrivacyAgreement: ko.observable(""), + BillingCycle: ko.observable(), + AvailableBillingCycleTypes: ko.observableArray([{ name: 'Annual', id: 0 }, { name: 'Monthly', id: 1 }]) }; }; @@ -153,6 +155,8 @@ Microsoft.WebPortal.BrandingSetupPresenter.prototype.onSaveBranding = function ( formData.append("InstrumentationKey", this.viewModel.InstrumentationKey()); formData.append("PrivacyAgreement", this.viewModel.PrivacyAgreement()); + formData.append("BillingCycle", this.viewModel.BillingCycle()); + var saveBrandingServerCall = this.webPortal.ServerCallManager.create(this.feature, function () { return $.ajax({ @@ -320,6 +324,7 @@ Microsoft.WebPortal.BrandingSetupPresenter.prototype._updateViewModel = function this.viewModel.HeaderImage(this.existingBrandingConfiguration.HeaderImage); this.viewModel.InstrumentationKey(this.existingBrandingConfiguration.InstrumentationKey); this.viewModel.PrivacyAgreement(this.existingBrandingConfiguration.PrivacyAgreement); + this.viewModel.BillingCycle(this.existingBrandingConfiguration.BillingCycle.toString()); }; @@ -354,7 +359,8 @@ Microsoft.WebPortal.BrandingSetupPresenter.prototype._setupActions = function () this.viewModel.OrganizationLogo() !== this.existingBrandingConfiguration.OrganizationLogo | this.viewModel.HeaderImage() !== this.existingBrandingConfiguration.HeaderImage | this.viewModel.InstrumentationKey() !== this.existingBrandingConfiguration.InstrumentationKey | - this.viewModel.PrivacyAgreement() !== this.existingBrandingConfiguration.PrivacyAgreement; + this.viewModel.PrivacyAgreement() !== this.existingBrandingConfiguration.PrivacyAgreement | + this.viewModel.BillingCycle() !== this.existingBrandingConfiguration.BillingCycle; isFormUpdated = isFormUpdated | this.viewModel.ContactUs.Email() !== this.existingBrandingConfiguration.ContactUs.Email | this.viewModel.ContactUs.Phone() !== this.existingBrandingConfiguration.ContactUs.Phone; diff --git a/src/Storefront/Scripts/Plugins/OfferListPresenter.js b/src/Storefront/Scripts/Plugins/OfferListPresenter.js index 295f26f..e48f768 100644 --- a/src/Storefront/Scripts/Plugins/OfferListPresenter.js +++ b/src/Storefront/Scripts/Plugins/OfferListPresenter.js @@ -123,7 +123,8 @@ Microsoft.WebPortal.OfferListPresenter.prototype.onCellClicked = function (colum Features: row.PartnerOffer.Features, Summary: row.PartnerOffer.Summary, Logo: row.PartnerOffer.LogoUri, - Thumbnail: row.PartnerOffer.ThumbnailUri + Thumbnail: row.PartnerOffer.ThumbnailUri, + DisplayIndex: row.PartnerOffer.DisplayIndex }; // go to the update offer feature and pass it the clicked offer @@ -255,6 +256,7 @@ Microsoft.WebPortal.OfferListPresenter.prototype._retrievePartnerOffers = functi var offerToPush = { PartnerOffer: partnerOffers[i], FormattedPrice: Globalize.format(partnerOffers[i].Price, "c"), + DisplayIndex: partnerOffers[i].DisplayIndex, MicrosoftOffer: function () { for (var j in microsoftOffers) { if (partnerOffers[i].MicrosoftOfferId === microsoftOffers[j].Offer.Id) { diff --git a/src/Storefront/Storefront.csproj b/src/Storefront/Storefront.csproj index de47e3a..a713ffe 100644 --- a/src/Storefront/Storefront.csproj +++ b/src/Storefront/Storefront.csproj @@ -313,6 +313,7 @@ + diff --git a/src/Storefront/Views/Controls/AddSubscriptions.cshtml b/src/Storefront/Views/Controls/AddSubscriptions.cshtml index a73e852..c491f95 100644 --- a/src/Storefront/Views/Controls/AddSubscriptions.cshtml +++ b/src/Storefront/Views/Controls/AddSubscriptions.cshtml @@ -34,7 +34,7 @@ - + diff --git a/src/Storefront/Views/Controls/OfferTile.cshtml b/src/Storefront/Views/Controls/OfferTile.cshtml index 19ae52b..4e28d23 100644 --- a/src/Storefront/Views/Controls/OfferTile.cshtml +++ b/src/Storefront/Views/Controls/OfferTile.cshtml @@ -11,9 +11,9 @@ - + - + diff --git a/src/Storefront/Views/Shared/AddOrUpdateOffer.cshtml b/src/Storefront/Views/Shared/AddOrUpdateOffer.cshtml index 2f6367f..65a2b18 100644 --- a/src/Storefront/Views/Shared/AddOrUpdateOffer.cshtml +++ b/src/Storefront/Views/Shared/AddOrUpdateOffer.cshtml @@ -54,12 +54,22 @@ - @Resources.YourAnnualPriceCaption + @(ViewBag.BillingCycle == Microsoft.Store.PartnerCenter.Storefront.Models.BillingCycleType.Annual ? Resources.YourAnnualPriceCaption : Resources.YourMonthlyPriceCaption) + + + + @Resources.YourDisplayIndexCaption + + + + + + @Resources.FeaturesCaption @@ -113,7 +123,7 @@
- +
@@ -154,6 +164,9 @@ OfferPrice: { required: true, validateprice:true + }, + OfferDisplayIndex: { + required: true } }, messages: { diff --git a/src/Storefront/Views/Shared/BrandingSetup.cshtml b/src/Storefront/Views/Shared/BrandingSetup.cshtml index a6ed4a1..a63038a 100644 --- a/src/Storefront/Views/Shared/BrandingSetup.cshtml +++ b/src/Storefront/Views/Shared/BrandingSetup.cshtml @@ -106,7 +106,7 @@ @Resources.AgreementUserIdFieldHeader -
+
@Resources.AgreementUserIdSubText @@ -119,6 +119,15 @@ @Resources.InstrumentationKeyFieldSubText + + + @Resources.BillingCycleFieldHeader + + +
+ @Resources.BillingCycleFieldSubText + + diff --git a/src/Storefront/Views/Shared/OfferList.cshtml b/src/Storefront/Views/Shared/OfferList.cshtml index 6102b1b..cd8101b 100644 --- a/src/Storefront/Views/Shared/OfferList.cshtml +++ b/src/Storefront/Views/Shared/OfferList.cshtml @@ -25,12 +25,13 @@

@Resources.PriceHeader
+ @Resources.DisplayIndexHeader
@@ -95,7 +95,7 @@
- + @Resources.SummaryListRowCaption
    diff --git a/src/Storefront/Views/Shared/ProcessOrder.cshtml b/src/Storefront/Views/Shared/ProcessOrder.cshtml index b04498a..223b82c 100644 --- a/src/Storefront/Views/Shared/ProcessOrder.cshtml +++ b/src/Storefront/Views/Shared/ProcessOrder.cshtml @@ -87,7 +87,7 @@ @Resources.SubscriptionSummaryRateCaption
- @Resources.AnnualPriceCaption + @(ViewBag.BillingCycle == Microsoft.Store.PartnerCenter.Storefront.Models.BillingCycleType.Annual ? Resources.AnnualPriceCaption : Resources.MonthlyPriceCaption)
- + diff --git a/src/Storefront/Views/Shared/Subscriptions.cshtml b/src/Storefront/Views/Shared/Subscriptions.cshtml index baedf73..bd68dbf 100644 --- a/src/Storefront/Views/Shared/Subscriptions.cshtml +++ b/src/Storefront/Views/Shared/Subscriptions.cshtml @@ -31,7 +31,7 @@ @Resources.SubscriptionSummaryRateCaption - *@Resources.SubscriptionSummaryAnnualPricePrefixCaption + *@(ViewBag.BillingCycle == Microsoft.Store.PartnerCenter.Storefront.Models.BillingCycleType.Annual ? Resources.SubscriptionSummaryAnnualPricePrefixCaption : Resources.SubscriptionSummaryMonthlyPricePrefixCaption) @@ -41,7 +41,7 @@
- + diff --git a/src/Storefront/Views/Shared/UpdateSubscriptions.cshtml b/src/Storefront/Views/Shared/UpdateSubscriptions.cshtml index 26bfd80..2d3ef67 100644 --- a/src/Storefront/Views/Shared/UpdateSubscriptions.cshtml +++ b/src/Storefront/Views/Shared/UpdateSubscriptions.cshtml @@ -26,7 +26,7 @@ - +