/// <summary> /// Rolls back the order that was placed. /// </summary> /// <returns>A task.</returns> public async Task RollbackAsync() { if (Result != null) { // suspend all subscriptions that resulted from placing the order IEnumerable <Task> suspensionTasks = Result.LineItems.Select <OrderLineItem, Task>(orderLineItem => new TaskFactory().StartNew(async() => { try { Subscriptions.ISubscription subscriptionOperations = 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).ConfigureAwait(false); 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).ConfigureAwait(false); } catch (Exception exception) { if (exception.IsFatal()) { throw; } Trace.TraceError("PlaceOrder.RollbackAsync: awaiting all suspension tasks failed: {0}", exception); } Result = null; } }
/// <summary> /// Normalizes an order to renew a subscription. /// </summary> /// <returns>Normalized order.</returns> public async Task <OrderViewModel> NormalizeRenewSubscriptionOrderAsync() { OrderViewModel order = 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 CustomerSubscriptionEntity subscriptionToAugment = await GetSubscriptionAsync(subscriptionId, order.CustomerId).ConfigureAwait(false); // retrieve the partner offer this subscription relates to, we need to know the current price PartnerOffer partnerOffer = await ApplicationDomain.Instance.OffersRepository.RetrieveAsync(subscriptionToAugment.PartnerOfferId).ConfigureAwait(false); if (partnerOffer.IsInactive) { // renewing deleted offers is prohibited throw new PartnerDomainException(ErrorCode.PurchaseDeletedOfferNotAllowed).AddDetail("Id", partnerOffer.Id); } // retrieve the subscription from Partner Center Subscriptions.ISubscription subscriptionOperations = ApplicationDomain.Instance.PartnerCenterClient.Customers.ById(order.CustomerId).Subscriptions.ById(subscriptionId); PartnerCenter.Models.Subscriptions.Subscription partnerCenterSubscription = await subscriptionOperations.GetAsync().ConfigureAwait(false); List <OrderSubscriptionItemViewModel> resultOrderSubscriptions = new List <OrderSubscriptionItemViewModel> { 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(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 CustomerSubscriptionEntity subscriptionToAugment = await GetSubscriptionAsync(subscriptionId, order.CustomerId).ConfigureAwait(false); // retrieve the partner offer this subscription relates to, we need to know the current price PartnerOffer partnerOffer = await ApplicationDomain.Instance.OffersRepository.RetrieveAsync(subscriptionToAugment.PartnerOfferId).ConfigureAwait(false); if (partnerOffer.IsInactive) { // renewing deleted offers is prohibited throw new PartnerDomainException(ErrorCode.PurchaseDeletedOfferNotAllowed).AddDetail("Id", partnerOffer.Id); } // retrieve the subscription from Partner Center Subscriptions.ISubscription subscriptionOperations = ApplicationDomain.Instance.PartnerCenterClient.Customers.ById(order.CustomerId).Subscriptions.ById(subscriptionId); PartnerCenter.Models.Subscriptions.Subscription partnerCenterSubscription = await subscriptionOperations.GetAsync().ConfigureAwait(false); // 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> { new OrderSubscriptionItemViewModel() { OfferId = subscriptionId, SubscriptionId = subscriptionId, PartnerOfferId = subscriptionToAugment.PartnerOfferId, SubscriptionExpiryDate = subscriptionToAugment.ExpiryDate, Quantity = seatsToPurchase, SeatPrice = proratedSeatCharge, SubscriptionName = partnerOffer.Title } }; orderResult.Subscriptions = resultOrderSubscriptions; return(orderResult); }
/// <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(ApplicationDomain, order); order = await orderNormalizer.NormalizeRenewSubscriptionOrderAsync().ConfigureAwait(false); 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 Subscriptions.ISubscription subscriptionOperations = ApplicationDomain.PartnerCenterClient.Customers.ById(CustomerId).Subscriptions.ById(subscriptionId); PartnerCenter.Models.Subscriptions.Subscription partnerCenterSubscription = await subscriptionOperations.GetAsync().ConfigureAwait(false); // 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 AuthorizePayment paymentAuthorization = new AuthorizePayment(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( ApplicationDomain.CustomerPurchasesRepository, new CustomerPurchaseEntity(CommerceOperationType.Renewal, Guid.NewGuid().ToString(), CustomerId, subscriptionId, partnerCenterSubscription.Quantity, partnerOfferPrice, rightNow))); 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, expirationDate))); // add a capture payment to the transaction pipeline subTransactions.Add(new CapturePayment(PaymentGateway, () => paymentAuthorization.Result)); // run the pipeline await RunAggregatedTransaction(subTransactions).ConfigureAwait(false); TransactionResultLineItem renewSubscriptionResult = new TransactionResultLineItem( subscriptionId, partnerOfferId, partnerCenterSubscription.Quantity, partnerOfferPrice, totalCharge); return(new TransactionResult( new TransactionResultLineItem[] { renewSubscriptionResult }, rightNow)); }