diff --git a/nce-bulk-migration-tool/NCEBulkMigrationTool/CustomerProvider.cs b/nce-bulk-migration-tool/NCEBulkMigrationTool/CustomerProvider.cs index c913a42..8f87d9a 100644 --- a/nce-bulk-migration-tool/NCEBulkMigrationTool/CustomerProvider.cs +++ b/nce-bulk-migration-tool/NCEBulkMigrationTool/CustomerProvider.cs @@ -32,7 +32,10 @@ internal class CustomerProvider : ICustomerProvider var authenticationResult = await this.tokenProvider.GetTokenAsync(); Console.WriteLine("Token generated..."); - var httpClient = new HttpClient(); + var httpClient = new HttpClient + { + BaseAddress = new Uri(Routes.BaseUrl) + }; httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken); httpClient.DefaultRequestHeaders.Add(Constants.PartnerCenterClientHeader, Constants.ClientName); @@ -52,7 +55,7 @@ internal class CustomerProvider : ICustomerProvider route = $"{Routes.GetCustomers}&seekOperation=next"; } - request.RequestUri = new Uri(route); + request.RequestUri = new Uri(route, UriKind.Relative); var response = await httpClient.SendAsync(request).ConfigureAwait(false); if (response.StatusCode == HttpStatusCode.BadRequest) diff --git a/nce-bulk-migration-tool/NCEBulkMigrationTool/INewCommerceMigrationScheduleProvider.cs b/nce-bulk-migration-tool/NCEBulkMigrationTool/INewCommerceMigrationScheduleProvider.cs new file mode 100644 index 0000000..c05f4c4 --- /dev/null +++ b/nce-bulk-migration-tool/NCEBulkMigrationTool/INewCommerceMigrationScheduleProvider.cs @@ -0,0 +1,36 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// ----------------------------------------------------------------------- + +namespace NCEBulkMigrationTool +{ + internal interface INewCommerceMigrationScheduleProvider + { + /// + /// Exports legacy commerce subscriptions with their migration eligibility to an output CSV file so that + /// they can be scheduled as needed. + /// + /// Bool indicating success/ failure. + Task ValidateAndGetSubscriptionsToScheduleMigrationAsync(); + + /// + /// Uploads New Commerce Migration Schedules based on CSV files in the input folders and writes the schedule migration data to a new CSV file. + /// + /// Bool indicating success/ failure. + Task UploadNewCommerceMigrationSchedulesAsync(); + + /// + /// Exports all the New Commerce Migration Schedules for the given input list of customers. + /// + /// Bool indicating success/ failure. + Task ExportNewCommerceMigrationSchedulesAsync(); + + /// + /// Cancels the New Commerce Migration Schedules based on the CSV input files and writes the output with the updated Schedule Migration Status. + /// + /// Bool indicating success/ failure. + Task CancelNewCommerceMigrationSchedulesAsync(); + } +} \ No newline at end of file diff --git a/nce-bulk-migration-tool/NCEBulkMigrationTool/Models.cs b/nce-bulk-migration-tool/NCEBulkMigrationTool/Models.cs index 4d68615..4c0c4e7 100644 --- a/nce-bulk-migration-tool/NCEBulkMigrationTool/Models.cs +++ b/nce-bulk-migration-tool/NCEBulkMigrationTool/Models.cs @@ -85,6 +85,8 @@ internal record MigrationRequest public int SeatCount { get; set; } + public DateTime? CustomTermEndDate { get; set; } + public bool AddOn { get; init; } public string BaseSubscriptionId { get; init; } = string.Empty; @@ -158,6 +160,8 @@ public record NewCommerceMigration public bool PurchaseFullTerm { get; set; } + public DateTime? CustomTermEndDate { get; set; } + public string ExternalReferenceId { get; set; } = string.Empty; public IEnumerable AddOnMigrations { get; set; } = Enumerable.Empty(); @@ -200,6 +204,8 @@ public record MigrationResult public int NCESeatCount { get; set; } + public DateTime? CustomTermEndDate { get; set; } + public int? ErrorCode { get; set; } = null; public string ErrorReason { get; init; } = string.Empty; @@ -223,6 +229,8 @@ public record NewCommerceEligibility public string CatalogItemId { get; set; } = string.Empty; + public DateTime? CustomTermEndDate { get; set; } + public IEnumerable Errors { get; set; } = Enumerable.Empty(); public IEnumerable AddOnMigrations { get; set; } = Enumerable.Empty(); @@ -242,6 +250,145 @@ public record CustomerErrorResponse public string Message { get; set; } = string.Empty; } +public record NewCommerceMigrationSchedule +{ + public string Id { get; set; } = string.Empty; + + public string CurrentSubscriptionId { get; set; } = string.Empty; + + public string Status { get; set; } = string.Empty; + + public string CustomerTenantId { get; set; } = string.Empty; + + public string CatalogItemId { get; set; } = string.Empty; + + public DateTime? SubscriptionEndDate { get; set; } + + public int Quantity { get; set; } + + public string TermDuration { get; set; } = string.Empty; + + public string BillingCycle { get; set; } = string.Empty; + + public bool PurchaseFullTerm { get; set; } + + public DateTime? CustomTermEndDate { get; set; } + + public DateTime? TargetDate { get; set; } + + public bool? MigrateOnRenewal { get; set; } + + public DateTimeOffset CreatedTime { get; set; } + + public DateTimeOffset LastModifiedTime { get; set; } + + public string ExternalReferenceId { get; set; } = string.Empty; + + public string NewCommerceMigrationId { get; set; } = string.Empty; + + public string ErrorDescription { get; set; } = string.Empty; + + public int? ErrorCode { get; set; } + + public IEnumerable AddOnMigrationSchedules { get; set; } = Enumerable.Empty(); +} + +internal record ScheduleMigrationRequest +{ + public string PartnerTenantId { get; init; } = string.Empty; + + public string IndirectResellerMpnId { get; init; } = string.Empty; + + public string CustomerName { get; init; } = string.Empty; + + public Guid CustomerTenantId { get; init; } + + public string LegacySubscriptionId { get; init; } = string.Empty; + + public string LegacySubscriptionName { get; init; } = string.Empty; + + public string LegacyProductName { get; init; } = string.Empty; + + public DateTime ExpirationDate { get; init; } + + public bool AutoRenewEnabled { get; init; } + + public bool AddOn { get; init; } + + public string BaseSubscriptionId { get; init; } = string.Empty; + + public bool MigrationEligible { get; set; } + + public string NcePsa { get; set; } = string.Empty; + + public string CurrentTerm { get; init; } = string.Empty; + + public string CurrentBillingPlan { get; init; } = string.Empty; + + public int CurrentSeatCount { get; set; } + + public bool StartNewTermInNce { get; init; } + + public string Term { get; init; } = string.Empty; + + public string BillingPlan { get; init; } = string.Empty; + + public int SeatCount { get; set; } + + public DateTime? CustomTermEndDate { get; set; } + + public DateTime? TargetDate { get; set; } + + public bool? MigrateOnRenewal { get; set; } + + public string MigrationIneligibilityReason { get; set; } = string.Empty; +} + +public record ScheduleMigrationResult +{ + public string PartnerTenantId { get; init; } = string.Empty; + + public string IndirectResellerMpnId { get; init; } = string.Empty; + + public string CustomerName { get; init; } = string.Empty; + + public Guid CustomerTenantId { get; init; } + + public string LegacySubscriptionId { get; init; } = string.Empty; + + public string LegacySubscriptionName { get; init; } = string.Empty; + + public string LegacyProductName { get; init; } = string.Empty; + + public DateTime ExpirationDate { get; init; } + + public bool AddOn { get; init; } + + public bool StartedNewTermInNce { get; init; } + + public string NCETermDuration { get; init; } = string.Empty; + + public string NCEBillingPlan { get; init; } = string.Empty; + + public int NCESeatCount { get; set; } + + public DateTime? CustomTermEndDate { get; set; } + + public DateTime? TargetDate { get; set; } + + public bool? MigrateOnRenewal { get; set; } + + public string MigrationScheduleStatus { get; init; } = string.Empty; + + public string MigrationScheduleId { get; init; } = string.Empty; + + public string BatchId { get; init; } = string.Empty; + + public int? ErrorCode { get; set; } = null; + + public string ErrorReason { get; init; } = string.Empty; +} + public enum NewCommerceEligibilityErrorCode { SubscriptionStatusNotActive = 0, @@ -261,6 +408,14 @@ public enum NewCommerceEligibilityErrorCode SubscriptionActiveForLessThanOneMonth = 7, TradeStatusNotAllowed = 8, + + InvalidPartnerIdOnRecord = 9, + + MigrationsWithinLast24hOfTermOnlyAllowedForFullTerm = 10, + + InvalidCustomTermEndDate = 11, + + CustomTermEndDateMustAlignWithExistingSubscriptionOrCalendarMonth = 12, } [JsonConverter(typeof(JsonStringEnumConverter))] @@ -277,6 +432,8 @@ internal enum SubscriptionStatus Expired = 4, Pending = 5, + + Disabled = 6, } [JsonConverter(typeof(JsonStringEnumConverter))] @@ -310,28 +467,58 @@ internal record Constants internal record Routes { + /// + /// Base url for the partner center Apis. + /// + public const string BaseUrl = "https://api.partnercenter.microsoft.com"; + /// /// Get customers route. /// - public const string GetCustomers = "https://api.partnercenter.microsoft.com/v1/customers?size=500"; + public const string GetCustomers = "/v1/customers?size=500"; /// /// Get subscriptions route, 0 represents customerId. /// - public const string GetSubscriptions = "https://api.partnercenter.microsoft.com/v1/customers/{0}/subscriptions"; + public const string GetSubscriptions = "/v1/customers/{0}/subscriptions"; /// /// Get new commerce migrations route, 0 represents customerId, 1 represents newCommerceMigrationId. /// - public const string GetNewCommerceMigration = "https://api.partnercenter.microsoft.com/v1/customers/{0}/migrations/newcommerce/{1}"; + public const string GetNewCommerceMigration = "/v1/customers/{0}/migrations/newcommerce/{1}"; /// /// Validate migration eligibility route, 0 represents customerId. /// - public const string ValidateMigrationEligibility = "https://api.partnercenter.microsoft.com/v1/customers/{0}/migrations/newcommerce/validate"; + public const string ValidateMigrationEligibility = "/v1/customers/{0}/migrations/newcommerce/validate"; /// /// Post new commerce migration, 0 represents customerId. /// - public const string PostNewCommerceMigration = "https://api.partnercenter.microsoft.com/v1/customers/{0}/migrations/newcommerce"; + public static string PostNewCommerceMigration = "/v1/customers/{0}/migrations/newcommerce"; + + /// + /// Get new commerce migrations route, 0 represents customerId, 1 represents newCommerceMigrationScheduleId. + /// + public const string GetNewCommerceMigrationSchedule = "/v1/customers/{0}/migrations/newcommerce/schedules/{1}"; + + /// + /// Post new commerce migration schedule, 0 represents customerId. + /// + public const string PostNewCommerceMigrationSchedule = "/v1/customers/{0}/migrations/newcommerce/schedules"; + + /// + /// Update new commerce migration schedule, 0 represents customerId, 1 represents newCommerceMigrationScheduleId. + /// + public const string UpdateNewCommerceMigrationSchedule = "/v1/customers/{0}/migrations/newcommerce/schedules/{1}"; + + /// + /// Get new commerce migration schedules route. + /// + public const string GetNewCommerceMigrationSchedules = "/v1/migrations/newcommerce/schedules"; + + /// + /// Cancel new commerce migration schedule, 0 represents customerId, 1 represents newCommerceMigrationScheduleId. + /// + public const string CancelNewCommerceMigrationSchedule = "/v1/customers/{0}/migrations/newcommerce/schedules/{1}/cancel"; } \ No newline at end of file diff --git a/nce-bulk-migration-tool/NCEBulkMigrationTool/NewCommerceMigrationProvider.cs b/nce-bulk-migration-tool/NCEBulkMigrationTool/NewCommerceMigrationProvider.cs index 25b0b16..a7f7a17 100644 --- a/nce-bulk-migration-tool/NCEBulkMigrationTool/NewCommerceMigrationProvider.cs +++ b/nce-bulk-migration-tool/NCEBulkMigrationTool/NewCommerceMigrationProvider.cs @@ -46,7 +46,10 @@ internal class NewCommerceMigrationProvider : INewCommerceMigrationProvider var migrations = new ConcurrentBag>(); - var httpClient = new HttpClient(); + var httpClient = new HttpClient + { + BaseAddress = new Uri(Routes.BaseUrl) + }; httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken); httpClient.DefaultRequestHeaders.Add(Constants.PartnerCenterClientHeader, Constants.ClientName); @@ -87,10 +90,10 @@ internal class NewCommerceMigrationProvider : INewCommerceMigrationProvider Console.WriteLine("Exporting migrations"); await csvProvider.ExportCsv(migrations.SelectMany(m => m), $"{Constants.OutputFolderPath}/migrations/{processedFileName}_{batchId}.csv"); - + File.Move(fileName, $"{Constants.InputFolderPath}/subscriptions/processed/{processedFileName}", true); - await Task.Delay(1000 * 60); + await Task.Delay(1000); Console.WriteLine($"Exported migrations at {Environment.CurrentDirectory}/{Constants.OutputFolderPath}/migrations/{processedFileName}_{batchId}.csv"); } @@ -115,7 +118,10 @@ internal class NewCommerceMigrationProvider : INewCommerceMigrationProvider var migrations = new ConcurrentBag(); - var httpClient = new HttpClient(); + var httpClient = new HttpClient + { + BaseAddress = new Uri(Routes.BaseUrl) + }; httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken); var options = new ParallelOptions() @@ -173,7 +179,7 @@ internal class NewCommerceMigrationProvider : INewCommerceMigrationProvider File.Move(fileName, $"{Constants.InputFolderPath}/migrations/processed/{processedFileName}", true); - await Task.Delay(1000 * 60); + await Task.Delay(1000); Console.WriteLine($"Exported migration status at {Environment.CurrentDirectory}/{Constants.OutputFolderPath}/migrationstatus/{processedFileName}.csv"); } @@ -226,7 +232,7 @@ internal class NewCommerceMigrationProvider : INewCommerceMigrationProvider } var result = this.PrepareMigrationResult(migrationResult, migrationResult.BatchId, migration, migrationError); - return (result!, migration?.AddOnMigrations ?? Enumerable.Empty()); + return (result, migration?.AddOnMigrations); } /// @@ -251,6 +257,7 @@ internal class NewCommerceMigrationProvider : INewCommerceMigrationProvider BillingCycle = migrationRequest.BillingPlan, TermDuration = migrationRequest.Term, ExternalReferenceId = batchId, + CustomTermEndDate = migrationRequest.CustomTermEndDate, }; // If they want to start a new term, then we should take the input from the file. @@ -317,6 +324,7 @@ internal class NewCommerceMigrationProvider : INewCommerceMigrationProvider BillingCycle = request.BillingPlan, TermDuration = request.Term, PurchaseFullTerm = request.StartNewTermInNce, + CustomTermEndDate = request.CustomTermEndDate, }); allAddOnMigrations.AddRange(addOnNewCommerceMigrations); @@ -363,18 +371,12 @@ internal class NewCommerceMigrationProvider : INewCommerceMigrationProvider /// The add on migration results. private List PrepareAddOnMigrationResult(IEnumerable addOnMigrationRequests, string batchId, NewCommerceMigration? newCommerceMigration, NewCommerceMigrationError? newCommerceMigrationError, List migrationResults) { - if(newCommerceMigration != null) + foreach (var addOnMigrationResponse in newCommerceMigration.AddOnMigrations) { - foreach (var addOnMigrationResponse in newCommerceMigration.AddOnMigrations) - { - var addOnMigrationRequest = addOnMigrationRequests.SingleOrDefault(n => n.LegacySubscriptionId.Equals(addOnMigrationResponse.CurrentSubscriptionId, StringComparison.OrdinalIgnoreCase)); - if(addOnMigrationRequest != null) - { - addOnMigrationResponse.Status = newCommerceMigration.Status; - addOnMigrationResponse.Id = newCommerceMigration.Id; - PrepareMigrationResult(addOnMigrationRequest, batchId, addOnMigrationResponse, newCommerceMigrationError, migrationResults); - } - } + var addOnMigrationRequest = addOnMigrationRequests.SingleOrDefault(n => n.LegacySubscriptionId.Equals(addOnMigrationResponse.CurrentSubscriptionId, StringComparison.OrdinalIgnoreCase)); + addOnMigrationResponse.Status = newCommerceMigration.Status; + addOnMigrationResponse.Id = newCommerceMigration.Id; + PrepareMigrationResult(addOnMigrationRequest, batchId, addOnMigrationResponse, newCommerceMigrationError, migrationResults); } return migrationResults; @@ -407,6 +409,7 @@ internal class NewCommerceMigrationProvider : INewCommerceMigrationProvider NCETermDuration = migrationRequest.Term, NCEBillingPlan = migrationRequest.BillingPlan, NCESeatCount = migrationRequest.SeatCount, + CustomTermEndDate = migrationRequest.CustomTermEndDate, ErrorCode = newCommerceMigrationError.Code, ErrorReason = newCommerceMigrationError.Description, }; @@ -432,6 +435,7 @@ internal class NewCommerceMigrationProvider : INewCommerceMigrationProvider NCETermDuration = newCommerceMigration.TermDuration, NCEBillingPlan = newCommerceMigration.BillingCycle, NCESeatCount = newCommerceMigration.Quantity, + CustomTermEndDate = newCommerceMigration.CustomTermEndDate, NCESubscriptionId = newCommerceMigration.NewCommerceSubscriptionId, BatchId = batchId, MigrationId = newCommerceMigration.Id, @@ -469,6 +473,7 @@ internal class NewCommerceMigrationProvider : INewCommerceMigrationProvider NCETermDuration = migrationResult.NCETermDuration, NCEBillingPlan = migrationResult.NCEBillingPlan, NCESeatCount = migrationResult.NCESeatCount, + CustomTermEndDate = migrationResult.CustomTermEndDate, ErrorCode = newCommerceMigrationError.Code, ErrorReason = newCommerceMigrationError.Description, }; @@ -491,6 +496,7 @@ internal class NewCommerceMigrationProvider : INewCommerceMigrationProvider NCETermDuration = newCommerceMigration.TermDuration, NCEBillingPlan = newCommerceMigration.BillingCycle, NCESeatCount = newCommerceMigration.Quantity, + CustomTermEndDate = newCommerceMigration.CustomTermEndDate, NCESubscriptionId = newCommerceMigration.NewCommerceSubscriptionId, BatchId = batchId, MigrationId = newCommerceMigration.Id, diff --git a/nce-bulk-migration-tool/NCEBulkMigrationTool/NewCommerceMigrationScheduleProvider.cs b/nce-bulk-migration-tool/NCEBulkMigrationTool/NewCommerceMigrationScheduleProvider.cs new file mode 100644 index 0000000..72ae27d --- /dev/null +++ b/nce-bulk-migration-tool/NCEBulkMigrationTool/NewCommerceMigrationScheduleProvider.cs @@ -0,0 +1,849 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// ----------------------------------------------------------------------- + +namespace NCEBulkMigrationTool +{ + internal class NewCommerceMigrationScheduleProvider : INewCommerceMigrationScheduleProvider + { + private static string partnerTenantId = string.Empty; + private readonly ITokenProvider tokenProvider; + private long subscriptionsCntr = 0; + + /// + /// The NewCommerceMigrationScheduleProvider constructor. + /// + /// The token provider. + public NewCommerceMigrationScheduleProvider(ITokenProvider tokenProvider) + { + this.tokenProvider = tokenProvider; + } + + /// + public async Task ValidateAndGetSubscriptionsToScheduleMigrationAsync() + { + subscriptionsCntr = 0; + var csvProvider = new CsvProvider(); + + using TextReader fileReader = File.OpenText($"{Constants.InputFolderPath}/customers.csv"); + using var csvReader = new CsvReader(fileReader, CultureInfo.InvariantCulture, leaveOpen: true); + var inputCustomers = csvReader.GetRecordsAsync(); + + ConcurrentBag> allMigrationRequests = new ConcurrentBag>(); + var failedCustomersBag = new ConcurrentBag(); + + var authenticationResult = await this.tokenProvider.GetTokenAsync(); + partnerTenantId = authenticationResult.TenantId; + + var httpClient = new HttpClient + { + BaseAddress = new Uri(Routes.BaseUrl) + }; + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken); + httpClient.DefaultRequestHeaders.Add(Constants.PartnerCenterClientHeader, Constants.ClientName); + + var options = new ParallelOptions() + { + MaxDegreeOfParallelism = 5 + }; + + await Parallel.ForEachAsync(inputCustomers, options, async (customer, cancellationToken) => + { + try + { + var subscriptions = await GetLegacySubscriptionsAsync(httpClient, customer, cancellationToken); + + Console.WriteLine($"Validating subscriptions eligibility for customer {customer.CompanyName}"); + var migrationEligibilities = await ValidateMigrationEligibility(customer, httpClient, options, subscriptions); + var migrationScheduleDetails = await GetMigrationScheduleAsync(customer, httpClient, options, migrationEligibilities); + allMigrationRequests.Add(migrationScheduleDetails.Select(a => a)); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to verify migration eligibility for customer {customer.CompanyName} {ex}"); + failedCustomersBag.Add(customer); + } + }); + + csvReader.Dispose(); + fileReader.Close(); + + Console.WriteLine("Exporting subscriptions"); + await csvProvider.ExportCsv(allMigrationRequests.SelectMany(m => m), $"{Constants.OutputFolderPath}/subscriptionsforschedule.csv"); + Console.WriteLine($"Exported subscriptions at {Environment.CurrentDirectory}/{Constants.OutputFolderPath}/subscriptionsforschedule.csv"); + + if (failedCustomersBag.Count > 0) + { + Console.WriteLine("Exporting failed customers"); + await csvProvider.ExportCsv(failedCustomersBag, $"{Constants.OutputFolderPath}/failedCustomers_schedulemigration.csv"); + Console.WriteLine($"Exported failed customers at {Environment.CurrentDirectory}/{Constants.OutputFolderPath}/failedCustomers_schedulemigration.csv"); + } + + return true; + } + + /// + public async Task UploadNewCommerceMigrationSchedulesAsync() + { + var csvProvider = new CsvProvider(); + + var inputFileNames = Directory.EnumerateFiles($"{Constants.InputFolderPath}/subscriptionsforschedule"); + var authenticationResult = await this.tokenProvider.GetTokenAsync(); + + foreach (var fileName in inputFileNames) + { + Console.WriteLine($"Processing file {fileName}"); + + using TextReader fileReader = File.OpenText(fileName); + using var csvReader = new CsvReader(fileReader, CultureInfo.InvariantCulture, leaveOpen: true); + var inputMigrationRequests = csvReader.GetRecords().ToList(); + + if (inputMigrationRequests.Count > 200) + { + Console.WriteLine($"There are too many migration requests in the file: {fileName}. The maximum limit for migration uploads per file is 200. Please fix the input file to continue..."); + continue; + } + + var migrations = new ConcurrentBag>(); + + var httpClient = new HttpClient + { + BaseAddress = new Uri(Routes.BaseUrl) + }; + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken); + httpClient.DefaultRequestHeaders.Add(Constants.PartnerCenterClientHeader, Constants.ClientName); + + var options = new ParallelOptions() + { + MaxDegreeOfParallelism = 5 + }; + + long subscriptionsCntr = 0; //The counter to track + var batchId = Guid.NewGuid().ToString(); + + var inputBaseMigrationRequests = inputMigrationRequests.Where(m => !m.AddOn && m.MigrationEligible); + var inputAddOnMigrationRequests = inputMigrationRequests.Where(m => m.AddOn && m.MigrationEligible); + + await Parallel.ForEachAsync(inputBaseMigrationRequests, options, async (migrationRequest, cancellationToken) => + { + try + { + List migrationResult; + migrationResult = await this.PostNewCommerceMigrationScheduleAsync(httpClient, migrationRequest, inputAddOnMigrationRequests, batchId, cancellationToken); + migrations.Add(migrationResult); + } + catch (Exception) + { + Console.WriteLine($"Migration for subscription: {migrationRequest.LegacySubscriptionId} failed."); + } + finally + { + Interlocked.Increment(ref subscriptionsCntr); + Console.WriteLine($"Processed {subscriptionsCntr} subscription migration requests.", subscriptionsCntr); + } + }); + + csvReader.Dispose(); + fileReader.Close(); + + var index = fileName.LastIndexOf('\\'); + var processedFileName = fileName[++index..]; + + Console.WriteLine("Exporting migrations"); + await csvProvider.ExportCsv(migrations.SelectMany(m => m), $"{Constants.OutputFolderPath}/schedulemigrations/{processedFileName}_{batchId}.csv"); + + File.Move(fileName, $"{Constants.InputFolderPath}/subscriptionsforschedule/processed/{processedFileName}", true); + + await Task.Delay(1000); + + Console.WriteLine($"Exported migrations at {Environment.CurrentDirectory}/{Constants.OutputFolderPath}/schedulemigrations/{processedFileName}_{batchId}.csv"); + } + + return true; + } + + /// + public async Task ExportNewCommerceMigrationSchedulesAsync() + { + var csvProvider = new CsvProvider(); + + using TextReader fileReader = File.OpenText($"{Constants.InputFolderPath}/customers.csv"); + using var csvReader = new CsvReader(fileReader, CultureInfo.InvariantCulture, leaveOpen: true); + var inputCustomers = csvReader.GetRecordsAsync(); + + ConcurrentBag> allMigrationSchedules = new(); + var failedCustomersBag = new ConcurrentBag(); + + var authenticationResult = await this.tokenProvider.GetTokenAsync(); + partnerTenantId = authenticationResult.TenantId; + + var httpClient = new HttpClient + { + BaseAddress = new Uri(Routes.BaseUrl) + }; + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken); + httpClient.DefaultRequestHeaders.Add(Constants.PartnerCenterClientHeader, Constants.ClientName); + + var options = new ParallelOptions() + { + MaxDegreeOfParallelism = 5 + }; + + await Parallel.ForEachAsync(inputCustomers, options, async (customer, cancellationToken) => + { + try + { + var getScheduleMigrationsRequest = new HttpRequestMessage(HttpMethod.Get, $"{Routes.GetNewCommerceMigrationSchedules}/?CustomerTenantId={customer.TenantId}"); + + getScheduleMigrationsRequest.Headers.Add("MS-CorrelationId", Guid.NewGuid().ToString()); + + var scheduleMigrationsResponse = await httpClient.SendAsync(getScheduleMigrationsRequest, cancellationToken).ConfigureAwait(false); + if (scheduleMigrationsResponse.StatusCode == HttpStatusCode.Unauthorized) + { + var authenticationResult = await this.tokenProvider.GetTokenAsync(); + httpClient.DefaultRequestHeaders.Clear(); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken); + httpClient.DefaultRequestHeaders.Add(Constants.PartnerCenterClientHeader, Constants.ClientName); + getScheduleMigrationsRequest = new HttpRequestMessage(HttpMethod.Get, $"{Routes.GetNewCommerceMigrationSchedules}/?CustomerTenantId={customer.TenantId}"); + getScheduleMigrationsRequest.Headers.Add("MS-CorrelationId", Guid.NewGuid().ToString()); + + scheduleMigrationsResponse = await httpClient.SendAsync(getScheduleMigrationsRequest).ConfigureAwait(false); + } + + scheduleMigrationsResponse.EnsureSuccessStatusCode(); + var newCommerceMigrationSchedules = await scheduleMigrationsResponse.Content.ReadFromJsonAsync>().ConfigureAwait(false); + allMigrationSchedules.Add(newCommerceMigrationSchedules); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to fetch migration schedules for customer {customer.CompanyName} {ex}"); + failedCustomersBag.Add(customer); + } + }); + + csvReader.Dispose(); + fileReader.Close(); + + Console.WriteLine("Exporting migration schedules"); + await csvProvider.ExportCsv(allMigrationSchedules.SelectMany(m => m), $"{Constants.OutputFolderPath}/schedulemigrations.csv"); + Console.WriteLine($"Exported schedule migrations at {Environment.CurrentDirectory}/{Constants.OutputFolderPath}/schedulemigrations.csv"); + + if (failedCustomersBag.Count > 0) + { + Console.WriteLine("Exporting failed customers"); + await csvProvider.ExportCsv(failedCustomersBag, "failedCustomers.csv"); + Console.WriteLine($"Exported failed customers at {Environment.CurrentDirectory}/failedCustomers.csv"); + } + + return true; + } + + /// + public async Task CancelNewCommerceMigrationSchedulesAsync() + { + var csvProvider = new CsvProvider(); + + var inputFileNames = Directory.EnumerateFiles($"{Constants.InputFolderPath}/cancelschedulemigrations"); + var authenticationResult = await this.tokenProvider.GetTokenAsync(); + + foreach (var fileName in inputFileNames) + { + Console.WriteLine($"Processing file {fileName}"); + + using TextReader fileReader = File.OpenText(fileName); + using var csvReader = new CsvReader(fileReader, CultureInfo.InvariantCulture, leaveOpen: true); + var inputMigrationSchedules = csvReader.GetRecords().ToList(); + + if (inputMigrationSchedules.Count > 200) + { + Console.WriteLine($"There are too many migration schedule requests in the file: {fileName}. The maximum limit for migration uploads per file is 200. Please fix the input file to continue..."); + continue; + } + + var migrationSchedules = new ConcurrentBag(); + + var httpClient = new HttpClient + { + BaseAddress = new Uri(Routes.BaseUrl) + }; + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken); + httpClient.DefaultRequestHeaders.Add(Constants.PartnerCenterClientHeader, Constants.ClientName); + + var options = new ParallelOptions() + { + MaxDegreeOfParallelism = 5 + }; + + long scheduleMigrationsCounter = 0; + var batchId = Guid.NewGuid().ToString(); + + var scheduledMigrationsToCancel = inputMigrationSchedules.Where(s => string.Equals(s.Status, "Cancel", StringComparison.OrdinalIgnoreCase)); + + await Parallel.ForEachAsync(scheduledMigrationsToCancel, options, async (scheduleMigration, cancellationToken) => + { + try + { + NewCommerceMigrationSchedule migrationScheduleResult; + migrationScheduleResult = await this.CancelNewCommerceMigrationScheduleAsync(httpClient, scheduleMigration, cancellationToken); + migrationSchedules.Add(migrationScheduleResult); + } + catch (Exception) + { + Console.WriteLine($"Cancel Migration Schedule for subscription: {scheduleMigration.CurrentSubscriptionId} failed."); + } + finally + { + Interlocked.Increment(ref scheduleMigrationsCounter); + Console.WriteLine($"Processed {scheduleMigrationsCounter} cancel schedule migration requests."); + } + }); + + csvReader.Dispose(); + fileReader.Close(); + + var index = fileName.LastIndexOf('\\'); + var processedFileName = fileName[++index..]; + + Console.WriteLine("Exporting cancelled scheduled migrations"); + await csvProvider.ExportCsv(migrationSchedules.Select(s => s), $"{Constants.OutputFolderPath}/cancelschedulemigrations/{processedFileName}_{batchId}.csv"); + + File.Move(fileName, $"{Constants.InputFolderPath}/cancelschedulemigrations/processed/{processedFileName}", true); + + await Task.Delay(1000); + + Console.WriteLine($"Exported cancelled schedule migrations at {Environment.CurrentDirectory}/{Constants.OutputFolderPath}/cancelschedulemigrations/{processedFileName}_{batchId}.csv"); + } + + return true; + } + + private async Task> GetLegacySubscriptionsAsync(HttpClient httpClient, CompanyProfile customer, CancellationToken cancellationToken) + { + var allSubscriptions = await this.GetSubscriptionsAsync(httpClient, customer, cancellationToken).ConfigureAwait(false); + var subscriptions = allSubscriptions.Where(s => Guid.TryParse(s.OfferId, out _)); + + return subscriptions; + } + + private async Task> GetSubscriptionsAsync(HttpClient httpClient, CompanyProfile customer, CancellationToken cancellationToken) + { + var subscriptionRequest = new HttpRequestMessage(HttpMethod.Get, string.Format(Routes.GetSubscriptions, customer.TenantId)); + + subscriptionRequest.Headers.Add("MS-CorrelationId", Guid.NewGuid().ToString()); + + var subscriptionResponse = await httpClient.SendAsync(subscriptionRequest, cancellationToken).ConfigureAwait(false); + if (subscriptionResponse.StatusCode == HttpStatusCode.Unauthorized) + { + var authenticationResult = await this.tokenProvider.GetTokenAsync(); + httpClient.DefaultRequestHeaders.Clear(); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken); + httpClient.DefaultRequestHeaders.Add(Constants.PartnerCenterClientHeader, Constants.ClientName); + subscriptionRequest = new HttpRequestMessage(HttpMethod.Get, string.Format(Routes.GetSubscriptions, customer.TenantId)); + subscriptionRequest.Headers.Add("MS-CorrelationId", Guid.NewGuid().ToString()); + + subscriptionResponse = await httpClient.SendAsync(subscriptionRequest).ConfigureAwait(false); + } + + subscriptionResponse.EnsureSuccessStatusCode(); + var subscriptionCollection = await subscriptionResponse.Content.ReadFromJsonAsync>().ConfigureAwait(false); + + return subscriptionCollection!.Items; + } + + private async Task> ValidateMigrationEligibility(CompanyProfile customer, HttpClient httpClient, ParallelOptions options, IEnumerable subscriptions) + { + var baseSubscriptions = subscriptions.Where(s => string.IsNullOrWhiteSpace(s.ParentSubscriptionId)); + var addOns = subscriptions.Where(s => !string.IsNullOrWhiteSpace(s.ParentSubscriptionId)); + var migrationRequests = new ConcurrentBag(); + var addOnEligibilityList = new ConcurrentBag>(); + await Parallel.ForEachAsync(baseSubscriptions, options, async (subscription, cancellationToken) => + { + var payload = new NewCommerceMigration + { + CurrentSubscriptionId = subscription.Id, + }; + + var migrationRequest = new HttpRequestMessage(HttpMethod.Post, string.Format(Routes.ValidateMigrationEligibility, customer.TenantId)) + { + Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json") + }; + + migrationRequest.Headers.Add("MS-CorrelationId", Guid.NewGuid().ToString()); + + var migrationResponse = await httpClient.SendAsync(migrationRequest, cancellationToken).ConfigureAwait(false); + if (migrationResponse.StatusCode == HttpStatusCode.Unauthorized) + { + var authenticationResult = await this.tokenProvider.GetTokenAsync(); + httpClient.DefaultRequestHeaders.Clear(); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken); + httpClient.DefaultRequestHeaders.Add(Constants.PartnerCenterClientHeader, Constants.ClientName); + migrationRequest = new HttpRequestMessage(HttpMethod.Post, string.Format(Routes.ValidateMigrationEligibility, customer.TenantId)) + { + Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json") + }; + migrationRequest.Headers.Add("MS-CorrelationId", Guid.NewGuid().ToString()); + + migrationResponse = await httpClient.SendAsync(migrationRequest).ConfigureAwait(false); + } + + migrationResponse.EnsureSuccessStatusCode(); + var newCommerceEligibility = await migrationResponse.Content.ReadFromJsonAsync().ConfigureAwait(false); + if (newCommerceEligibility.AddOnMigrations.Any()) + { + addOnEligibilityList.Add(newCommerceEligibility.AddOnMigrations); + } + + migrationRequests.Add(PrepareMigrationRequest(customer, subscription, newCommerceEligibility!)); + + Interlocked.Increment(ref subscriptionsCntr); + Console.WriteLine($"Validated migration eligibility for {subscriptionsCntr} subscriptions."); + }); + + foreach (var addOn in addOns) + { + var addOnEligibility = addOnEligibilityList.SelectMany(a => a).SingleOrDefault(m => m.CurrentSubscriptionId.Equals(addOn.Id, StringComparison.OrdinalIgnoreCase)); + if (addOnEligibility != null) + { + migrationRequests.Add(PrepareMigrationRequest(customer, addOn, addOnEligibility)); + } + } + + return migrationRequests; + } + + private async Task> GetMigrationScheduleAsync(CompanyProfile customer, HttpClient httpClient, ParallelOptions options, IEnumerable scheduleMigrationRequests) + { + var baseSubscriptions = scheduleMigrationRequests.Where(s => string.IsNullOrWhiteSpace(s.BaseSubscriptionId)); + var addOns = scheduleMigrationRequests.Where(s => !string.IsNullOrWhiteSpace(s.BaseSubscriptionId)); + var migrationRequests = new ConcurrentBag(); + var addOnEligibilityList = new ConcurrentBag>(); + await Parallel.ForEachAsync(baseSubscriptions, options, async (subscription, cancellationToken) => + { + var getScheduleMigrationRequest = new HttpRequestMessage(HttpMethod.Get, $"{Routes.GetNewCommerceMigrationSchedules}?CustomerTenantId={customer.TenantId}&CurrentSubscriptionId={subscription.LegacySubscriptionId}"); + + getScheduleMigrationRequest.Headers.Add("MS-CorrelationId", Guid.NewGuid().ToString()); + + var migrationResponse = await httpClient.SendAsync(getScheduleMigrationRequest, cancellationToken).ConfigureAwait(false); + if (migrationResponse.StatusCode == HttpStatusCode.Unauthorized) + { + var authenticationResult = await this.tokenProvider.GetTokenAsync(); + httpClient.DefaultRequestHeaders.Clear(); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken); + httpClient.DefaultRequestHeaders.Add(Constants.PartnerCenterClientHeader, Constants.ClientName); + getScheduleMigrationRequest = new HttpRequestMessage(HttpMethod.Get, $"{Routes.GetNewCommerceMigrationSchedules}?CustomerTenantId={customer.TenantId}&CurrentSubscriptionId={subscription.LegacySubscriptionId}"); + getScheduleMigrationRequest.Headers.Add("MS-CorrelationId", Guid.NewGuid().ToString()); + + migrationResponse = await httpClient.SendAsync(getScheduleMigrationRequest).ConfigureAwait(false); + } + + migrationResponse.EnsureSuccessStatusCode(); + var newCommerceMigrationSchedule = (await migrationResponse.Content.ReadFromJsonAsync>().ConfigureAwait(false)).SingleOrDefault(); + + migrationRequests.Add(PrepareMigrationRequest(subscription, newCommerceMigrationSchedule)); + + if (newCommerceMigrationSchedule?.AddOnMigrationSchedules.Any() ?? false) + { + addOnEligibilityList.Add(newCommerceMigrationSchedule.AddOnMigrationSchedules); + } + }); + + foreach (var addOn in addOns) + { + var addOnEligibility = addOnEligibilityList.SelectMany(a => a).SingleOrDefault(m => m.CurrentSubscriptionId.Equals(addOn.LegacySubscriptionId, StringComparison.OrdinalIgnoreCase)); + migrationRequests.Add(PrepareMigrationRequest(addOn, addOnEligibility)); + } + + return migrationRequests; + } + + private static ScheduleMigrationRequest PrepareMigrationRequest(CompanyProfile companyProfile, Subscription subscription, NewCommerceEligibility newCommerceEligibility) + { + return new ScheduleMigrationRequest + { + PartnerTenantId = partnerTenantId, + IndirectResellerMpnId = subscription.PartnerId, + CustomerName = companyProfile.CompanyName, + CustomerTenantId = companyProfile.TenantId, + LegacySubscriptionId = subscription.Id, + LegacySubscriptionName = subscription.FriendlyName, + LegacyProductName = subscription.OfferName, + ExpirationDate = subscription.CommitmentEndDate, + AutoRenewEnabled = subscription.AutoRenewEnabled, + MigrationEligible = newCommerceEligibility.IsEligible, + NcePsa = newCommerceEligibility.CatalogItemId, + CurrentTerm = subscription.TermDuration, + CurrentBillingPlan = subscription.BillingCycle.ToString(), + CurrentSeatCount = subscription.Quantity, + StartNewTermInNce = false, + Term = subscription.TermDuration, + BillingPlan = subscription.BillingCycle.ToString(), + SeatCount = subscription.Quantity, + CustomTermEndDate = newCommerceEligibility.CustomTermEndDate, + AddOn = !string.IsNullOrWhiteSpace(subscription.ParentSubscriptionId), + BaseSubscriptionId = subscription.ParentSubscriptionId, + MigrationIneligibilityReason = newCommerceEligibility.Errors.Any() ? + string.Join(";", newCommerceEligibility.Errors.Select(e => e.Description)) : + string.Empty + }; + } + + private static ScheduleMigrationRequest PrepareMigrationRequest(ScheduleMigrationRequest scheduleMigrationRequest, NewCommerceMigrationSchedule? newCommerceMigrationSchedule) + { + if (newCommerceMigrationSchedule is not null) + { + return scheduleMigrationRequest with + { + StartNewTermInNce = newCommerceMigrationSchedule.PurchaseFullTerm, + Term = newCommerceMigrationSchedule.TermDuration, + BillingPlan = newCommerceMigrationSchedule.BillingCycle, + SeatCount = newCommerceMigrationSchedule.Quantity, + CustomTermEndDate = newCommerceMigrationSchedule.CustomTermEndDate, + TargetDate = newCommerceMigrationSchedule.TargetDate, + MigrateOnRenewal = newCommerceMigrationSchedule.MigrateOnRenewal, + }; + } + + return scheduleMigrationRequest; + } + + private async Task> PostNewCommerceMigrationScheduleAsync(HttpClient httpClient, ScheduleMigrationRequest migrationRequest, IEnumerable addOnMigrationRequests, string batchId, CancellationToken cancellationToken) + { + var newCommerceMigrationRequest = new HttpRequestMessage(HttpMethod.Post, string.Format(Routes.PostNewCommerceMigrationSchedule, migrationRequest.CustomerTenantId)); + + var getSchedulesRoute = $"{Routes.GetNewCommerceMigrationSchedules}?CurrentSubscriptionId={migrationRequest.LegacySubscriptionId}"; + var getSchedulesRequest = new HttpRequestMessage(HttpMethod.Get, getSchedulesRoute); + getSchedulesRequest.Headers.Add("MS-CorrelationId", Guid.NewGuid().ToString()); + var existingNewCommerceMigrationScheduleResponse = await httpClient.SendAsync(getSchedulesRequest).ConfigureAwait(false); + NewCommerceMigrationSchedule? existingSchedule = null; + + if (existingNewCommerceMigrationScheduleResponse.IsSuccessStatusCode) + { + existingSchedule = (await existingNewCommerceMigrationScheduleResponse.Content.ReadFromJsonAsync>().ConfigureAwait(false))?.FirstOrDefault(); + } + + newCommerceMigrationRequest.Headers.Add("MS-CorrelationId", Guid.NewGuid().ToString()); + + var newCommerceMigrationSchedule = new NewCommerceMigrationSchedule + { + CurrentSubscriptionId = migrationRequest.LegacySubscriptionId, + Quantity = migrationRequest.SeatCount, + BillingCycle = migrationRequest.BillingPlan, + TermDuration = migrationRequest.Term, + CustomTermEndDate = migrationRequest.CustomTermEndDate, + TargetDate = migrationRequest.TargetDate, + MigrateOnRenewal = migrationRequest.MigrateOnRenewal, + ExternalReferenceId = batchId, + }; + + // If they want to start a new term, then we should take the input from the file. + if (migrationRequest.StartNewTermInNce) + { + newCommerceMigrationSchedule.PurchaseFullTerm = true; + } + + if (existingSchedule is not null) + { + newCommerceMigrationRequest = new HttpRequestMessage(HttpMethod.Put, string.Format(Routes.UpdateNewCommerceMigrationSchedule, migrationRequest.CustomerTenantId, existingSchedule.Id)); + newCommerceMigrationSchedule = newCommerceMigrationSchedule with + { + Id = existingSchedule.Id, + }; + } + + newCommerceMigrationSchedule.AddOnMigrationSchedules = GetAddOnMigrationSchedules(migrationRequest.LegacySubscriptionId, addOnMigrationRequests); + + newCommerceMigrationRequest.Content = new StringContent(JsonSerializer.Serialize(newCommerceMigrationSchedule), Encoding.UTF8, "application/json"); + + var migrationResponse = await httpClient.SendAsync(newCommerceMigrationRequest, cancellationToken).ConfigureAwait(false); + if (migrationResponse.StatusCode == HttpStatusCode.Unauthorized) + { + var authenticationResult = await this.tokenProvider.GetTokenAsync(); + httpClient.DefaultRequestHeaders.Clear(); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken); + httpClient.DefaultRequestHeaders.Add(Constants.PartnerCenterClientHeader, Constants.ClientName); + if (existingSchedule is not null) + { + newCommerceMigrationRequest = new HttpRequestMessage(HttpMethod.Put, string.Format(Routes.UpdateNewCommerceMigrationSchedule, migrationRequest.CustomerTenantId, existingSchedule.Id)); + newCommerceMigrationSchedule = newCommerceMigrationSchedule with + { + Id = existingSchedule.Id, + }; + } + else + { + newCommerceMigrationRequest = new HttpRequestMessage(HttpMethod.Post, string.Format(Routes.PostNewCommerceMigrationSchedule, migrationRequest.CustomerTenantId)); + } + + newCommerceMigrationRequest.Content = new StringContent(JsonSerializer.Serialize(newCommerceMigrationSchedule), Encoding.UTF8, "application/json"); + + newCommerceMigrationRequest.Headers.Add("MS-CorrelationId", Guid.NewGuid().ToString()); + migrationResponse = await httpClient.SendAsync(newCommerceMigrationRequest).ConfigureAwait(false); + } + + NewCommerceMigrationError? migrationError = null; + NewCommerceMigrationSchedule? migration = null; + + if (migrationResponse.IsSuccessStatusCode) + { + migration = await migrationResponse.Content.ReadFromJsonAsync().ConfigureAwait(false); + } + else + { + migrationError = await migrationResponse.Content.ReadFromJsonAsync().ConfigureAwait(false); + } + + return this.PrepareMigrationResult(migrationRequest, addOnMigrationRequests, batchId, migration, migrationError); + } + + private async Task CancelNewCommerceMigrationScheduleAsync(HttpClient httpClient, NewCommerceMigrationSchedule newCommerceMigrationSchedule, CancellationToken cancellationToken) + { + var cancelNewCommerceMigrationRequest = new HttpRequestMessage(HttpMethod.Post, string.Format(Routes.CancelNewCommerceMigrationSchedule, newCommerceMigrationSchedule.CustomerTenantId, newCommerceMigrationSchedule.Id)); + cancelNewCommerceMigrationRequest.Headers.Add("MS-CorrelationId", Guid.NewGuid().ToString()); + + var migrationScheduleResponse = await httpClient.SendAsync(cancelNewCommerceMigrationRequest, cancellationToken).ConfigureAwait(false); + if (migrationScheduleResponse.StatusCode == HttpStatusCode.Unauthorized) + { + var authenticationResult = await this.tokenProvider.GetTokenAsync(); + httpClient.DefaultRequestHeaders.Clear(); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken); + httpClient.DefaultRequestHeaders.Add(Constants.PartnerCenterClientHeader, Constants.ClientName); + cancelNewCommerceMigrationRequest = new HttpRequestMessage(HttpMethod.Post, string.Format(Routes.CancelNewCommerceMigrationSchedule, newCommerceMigrationSchedule.CustomerTenantId, newCommerceMigrationSchedule.Id)); + cancelNewCommerceMigrationRequest.Headers.Add("MS-CorrelationId", Guid.NewGuid().ToString()); + cancelNewCommerceMigrationRequest.Content = new StringContent(JsonSerializer.Serialize(newCommerceMigrationSchedule), Encoding.UTF8, "application/json"); + migrationScheduleResponse = await httpClient.SendAsync(cancelNewCommerceMigrationRequest).ConfigureAwait(false); + } + + if (migrationScheduleResponse.IsSuccessStatusCode) + { + newCommerceMigrationSchedule = await migrationScheduleResponse.Content.ReadFromJsonAsync().ConfigureAwait(false); + } + else + { + var migrationScheduleError = await migrationScheduleResponse.Content.ReadFromJsonAsync().ConfigureAwait(false); + newCommerceMigrationSchedule.ErrorCode = migrationScheduleError.Code; + newCommerceMigrationSchedule.ErrorDescription = migrationScheduleError.Description; + } + + return newCommerceMigrationSchedule; + } + + private static IEnumerable GetAddOnMigrationSchedules(string currentSubscriptionId, IEnumerable addOnMigrationRequests) + { + if (!addOnMigrationRequests.Any()) + { + return Enumerable.Empty(); + } + + var allAddOnMigrations = new List(); + + var childAddOns = addOnMigrationRequests.Where(a => a.BaseSubscriptionId.Equals(currentSubscriptionId, StringComparison.OrdinalIgnoreCase)); + + if (childAddOns.Any()) + { + var addOnNewCommerceMigrations = childAddOns.Select(request => new NewCommerceMigrationSchedule + { + CurrentSubscriptionId = request.LegacySubscriptionId, + Quantity = request.SeatCount, + BillingCycle = request.BillingPlan, + TermDuration = request.Term, + PurchaseFullTerm = request.StartNewTermInNce, + CustomTermEndDate = request.CustomTermEndDate, + TargetDate = request.TargetDate, + MigrateOnRenewal = request.MigrateOnRenewal, + }); + + allAddOnMigrations.AddRange(addOnNewCommerceMigrations); + + foreach (var item in childAddOns) + { + var multiLevelAddons = GetAddOnMigrationSchedules(item.LegacySubscriptionId, addOnMigrationRequests); + allAddOnMigrations.AddRange(multiLevelAddons); + } + } + + return allAddOnMigrations; + } + + private List PrepareMigrationResult(ScheduleMigrationRequest migrationRequest, IEnumerable addOnMigrationRequests, string batchId, NewCommerceMigrationSchedule? newCommerceMigrationSchedule = null, NewCommerceMigrationError? newCommerceMigrationError = null) + { + var migrationResults = new List(); + PrepareMigrationResult(migrationRequest, batchId, newCommerceMigrationSchedule, newCommerceMigrationError, migrationResults); + + if (newCommerceMigrationSchedule?.AddOnMigrationSchedules.Any() == true) + { + PrepareAddOnMigrationResult(addOnMigrationRequests, batchId, newCommerceMigrationSchedule, newCommerceMigrationError, migrationResults); + } + + return migrationResults; + } + + private static void PrepareMigrationResult(ScheduleMigrationRequest migrationRequest, string batchId, NewCommerceMigrationSchedule? newCommerceMigrationSchedule, NewCommerceMigrationError? newCommerceMigrationError, List migrationResults) + { + if (newCommerceMigrationError != null) + { + var migrationResult = new ScheduleMigrationResult + { + PartnerTenantId = migrationRequest.PartnerTenantId, + IndirectResellerMpnId = migrationRequest.IndirectResellerMpnId, + CustomerName = migrationRequest.CustomerName, + CustomerTenantId = migrationRequest.CustomerTenantId, + LegacySubscriptionId = migrationRequest.LegacySubscriptionId, + LegacySubscriptionName = migrationRequest.LegacySubscriptionName, + LegacyProductName = migrationRequest.LegacyProductName, + ExpirationDate = migrationRequest.ExpirationDate, + AddOn = migrationRequest.AddOn, + StartedNewTermInNce = migrationRequest.StartNewTermInNce, + NCETermDuration = migrationRequest.Term, + NCEBillingPlan = migrationRequest.BillingPlan, + NCESeatCount = migrationRequest.SeatCount, + CustomTermEndDate = migrationRequest.CustomTermEndDate, + TargetDate = migrationRequest.TargetDate, + MigrateOnRenewal = migrationRequest.MigrateOnRenewal, + ErrorCode = newCommerceMigrationError.Code, + ErrorReason = newCommerceMigrationError.Description, + }; + + migrationResults.Add(migrationResult); + } + + if (newCommerceMigrationSchedule != null) + { + var migrationResult = new ScheduleMigrationResult + { + PartnerTenantId = migrationRequest.PartnerTenantId, + IndirectResellerMpnId = migrationRequest.IndirectResellerMpnId, + CustomerName = migrationRequest.CustomerName, + CustomerTenantId = migrationRequest.CustomerTenantId, + LegacySubscriptionId = migrationRequest.LegacySubscriptionId, + LegacySubscriptionName = migrationRequest.LegacySubscriptionName, + LegacyProductName = migrationRequest.LegacyProductName, + ExpirationDate = migrationRequest.ExpirationDate, + AddOn = migrationRequest.AddOn, + StartedNewTermInNce = migrationRequest.StartNewTermInNce, + NCETermDuration = newCommerceMigrationSchedule.TermDuration, + NCEBillingPlan = newCommerceMigrationSchedule.BillingCycle, + NCESeatCount = newCommerceMigrationSchedule.Quantity, + CustomTermEndDate = migrationRequest.CustomTermEndDate, + TargetDate = migrationRequest.TargetDate, + MigrateOnRenewal = migrationRequest.MigrateOnRenewal, + BatchId = batchId, + MigrationScheduleId = newCommerceMigrationSchedule.Id, + MigrationScheduleStatus = newCommerceMigrationSchedule.Status, + }; + + migrationResults.Add(migrationResult); + } + } + + private List PrepareAddOnMigrationResult(IEnumerable addOnMigrationRequests, string batchId, NewCommerceMigrationSchedule? newCommerceMigrationSchedule, NewCommerceMigrationError? newCommerceMigrationError, List migrationResults) + { + if (newCommerceMigrationSchedule != null && addOnMigrationRequests?.Any() == true) + { + foreach (var addOnMigrationResponse in newCommerceMigrationSchedule.AddOnMigrationSchedules) + { + var addOnMigrationRequest = addOnMigrationRequests.SingleOrDefault(n => n.LegacySubscriptionId.Equals(addOnMigrationResponse.CurrentSubscriptionId, StringComparison.OrdinalIgnoreCase)); + addOnMigrationResponse.Status = newCommerceMigrationSchedule.Status; + addOnMigrationResponse.Id = newCommerceMigrationSchedule.Id; + PrepareMigrationResult(addOnMigrationRequest, batchId, addOnMigrationResponse, newCommerceMigrationError, migrationResults); + } + } + + return migrationResults; + } + + private async Task<(ScheduleMigrationResult BaseMigrationResult, IEnumerable AddOnMigrationsResult)> GetNewCommerceMigrationScheduleByScheduleIdAsync(HttpClient httpClient, ScheduleMigrationResult migrationResult, CancellationToken cancellationToken) + { + // Validate that the migration result has a migrationId, if a migration didn't initiate the migrationId will be empty. + if (string.IsNullOrWhiteSpace(migrationResult.MigrationScheduleId)) + { + // We cannot determine the status, we should return this migration result. + return (migrationResult, Enumerable.Empty()); + } + + var getNewCommerceMigrationSchedule = new HttpRequestMessage(HttpMethod.Get, string.Format(Routes.GetNewCommerceMigrationSchedule, migrationResult.CustomerTenantId, migrationResult.MigrationScheduleId)); + + getNewCommerceMigrationSchedule.Headers.Add("MS-CorrelationId", Guid.NewGuid().ToString()); + + var migrationResponse = await httpClient.SendAsync(getNewCommerceMigrationSchedule, cancellationToken).ConfigureAwait(false); + if (migrationResponse.StatusCode == HttpStatusCode.Unauthorized) + { + var authenticationResult = await this.tokenProvider.GetTokenAsync(); + httpClient.DefaultRequestHeaders.Clear(); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken); + httpClient.DefaultRequestHeaders.Add(Constants.PartnerCenterClientHeader, Constants.ClientName); + getNewCommerceMigrationSchedule = new HttpRequestMessage(HttpMethod.Get, string.Format(Routes.GetNewCommerceMigrationSchedule, migrationResult.CustomerTenantId, migrationResult.MigrationScheduleId)); + getNewCommerceMigrationSchedule.Headers.Add("MS-CorrelationId", Guid.NewGuid().ToString()); + migrationResponse = await httpClient.SendAsync(getNewCommerceMigrationSchedule).ConfigureAwait(false); + } + + NewCommerceMigrationError? migrationError = null; + NewCommerceMigrationSchedule? migration = null; + + if (migrationResponse.IsSuccessStatusCode) + { + migration = await migrationResponse.Content.ReadFromJsonAsync().ConfigureAwait(false); + } + + var result = this.PrepareMigrationResult(migrationResult, migrationResult.BatchId, migration, migrationError); + return (result, migration?.AddOnMigrationSchedules); + } + + private ScheduleMigrationResult PrepareMigrationResult(ScheduleMigrationResult migrationResult, string batchId, NewCommerceMigrationSchedule? newCommerceMigrationSchedule = null, NewCommerceMigrationError? newCommerceMigrationError = null) + { + ScheduleMigrationResult result = new ScheduleMigrationResult(); + + if (newCommerceMigrationError != null) + { + result = new ScheduleMigrationResult + { + PartnerTenantId = migrationResult.PartnerTenantId, + IndirectResellerMpnId = migrationResult.IndirectResellerMpnId, + CustomerName = migrationResult.CustomerName, + CustomerTenantId = migrationResult.CustomerTenantId, + LegacySubscriptionId = migrationResult.LegacySubscriptionId, + LegacySubscriptionName = migrationResult.LegacySubscriptionName, + LegacyProductName = migrationResult.LegacyProductName, + ExpirationDate = migrationResult.ExpirationDate, + StartedNewTermInNce = migrationResult.StartedNewTermInNce, + NCETermDuration = migrationResult.NCETermDuration, + NCEBillingPlan = migrationResult.NCEBillingPlan, + NCESeatCount = migrationResult.NCESeatCount, + CustomTermEndDate = migrationResult.CustomTermEndDate, + ErrorCode = newCommerceMigrationError.Code, + ErrorReason = newCommerceMigrationError.Description, + }; + } + + if (newCommerceMigrationSchedule != null) + { + result = new ScheduleMigrationResult + { + PartnerTenantId = migrationResult.PartnerTenantId, + IndirectResellerMpnId = migrationResult.IndirectResellerMpnId, + CustomerName = migrationResult.CustomerName, + CustomerTenantId = migrationResult.CustomerTenantId, + LegacySubscriptionId = migrationResult.LegacySubscriptionId, + LegacySubscriptionName = migrationResult.LegacySubscriptionName, + LegacyProductName = migrationResult.LegacyProductName, + ExpirationDate = migrationResult.ExpirationDate, + StartedNewTermInNce = migrationResult.StartedNewTermInNce, + NCETermDuration = newCommerceMigrationSchedule.TermDuration, + NCEBillingPlan = newCommerceMigrationSchedule.BillingCycle, + NCESeatCount = newCommerceMigrationSchedule.Quantity, + CustomTermEndDate = newCommerceMigrationSchedule.CustomTermEndDate, + TargetDate = newCommerceMigrationSchedule.TargetDate, + MigrateOnRenewal = newCommerceMigrationSchedule.MigrateOnRenewal, + BatchId = batchId, + MigrationScheduleId = newCommerceMigrationSchedule.Id, + MigrationScheduleStatus = newCommerceMigrationSchedule.Status, + ErrorCode = newCommerceMigrationSchedule.ErrorCode, + ErrorReason = newCommerceMigrationSchedule.ErrorDescription, + }; + } + + return result; + } + } +} \ No newline at end of file diff --git a/nce-bulk-migration-tool/NCEBulkMigrationTool/Program.cs b/nce-bulk-migration-tool/NCEBulkMigrationTool/Program.cs index fe22965..4174d5e 100644 --- a/nce-bulk-migration-tool/NCEBulkMigrationTool/Program.cs +++ b/nce-bulk-migration-tool/NCEBulkMigrationTool/Program.cs @@ -15,7 +15,7 @@ if (args.Length == 2) } else { - AppId: +AppId: Console.WriteLine("Enter AppId"); appId = Console.ReadLine(); @@ -25,7 +25,7 @@ else goto AppId; } - Upn: +Upn: Console.WriteLine("Enter Upn"); upn = Console.ReadLine(); if (string.IsNullOrWhiteSpace(upn)) @@ -49,6 +49,7 @@ using IHost host = Host.CreateDefaultBuilder(args) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); }).Build(); await RunAsync(host.Services); @@ -57,11 +58,7 @@ await host.RunAsync(); static async Task RunAsync(IServiceProvider serviceProvider) { - Directory.CreateDirectory($"{Constants.InputFolderPath}/subscriptions/processed"); - Directory.CreateDirectory($"{Constants.InputFolderPath}/migrations/processed"); - Directory.CreateDirectory(Constants.OutputFolderPath); - - ShowOptions: +ShowOptions: Console.WriteLine("Please choose an option"); Console.WriteLine("1. Export customers"); @@ -69,23 +66,33 @@ static async Task RunAsync(IServiceProvider serviceProvider) Console.WriteLine("3. Upload migrations"); Console.WriteLine("4. Export migration status"); Console.WriteLine("5. Export NCE subscriptions"); - Console.WriteLine("6. Exit"); + Console.WriteLine("6. Export subscriptions with migration eligibility to schedule migrations"); + Console.WriteLine("7. Upload migration schedules"); + Console.WriteLine("8. Export schedule migrations"); + Console.WriteLine("9. Cancel schedule migrations"); + Console.WriteLine("10. Exit"); SelectOption: var option = Console.ReadLine(); - if (!short.TryParse(option, out short input) || !(input >= 1 && input <= 6)) + if (!short.TryParse(option, out short input) || !(input >= 1 && input <= 10)) { - Console.WriteLine("Invalid input, Please try again! Possible values are {1, 2, 3, 4, 5, 6}"); + Console.WriteLine("Invalid input, Please try again! Possible values are {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}"); goto SelectOption; } - if(input == 6) + if (input == 10) { Console.WriteLine("Exiting the app!"); Environment.Exit(Environment.ExitCode); } + Directory.CreateDirectory($"{Constants.InputFolderPath}/subscriptions/processed"); + Directory.CreateDirectory($"{Constants.InputFolderPath}/migrations/processed"); + Directory.CreateDirectory($"{Constants.InputFolderPath}/subscriptionsforschedule/processed"); + Directory.CreateDirectory($"{Constants.InputFolderPath}/cancelschedulemigrations/processed"); + Directory.CreateDirectory(Constants.OutputFolderPath); + Stopwatch stopwatch = Stopwatch.StartNew(); var result = input switch @@ -95,6 +102,10 @@ SelectOption: 3 => await serviceProvider.GetRequiredService().UploadNewCommerceMigrationsAsync(), 4 => await serviceProvider.GetRequiredService().ExportNewCommerceMigrationStatusAsync(), 5 => await serviceProvider.GetRequiredService().ExportModernSubscriptionsAsync(), + 6 => await serviceProvider.GetRequiredService().ValidateAndGetSubscriptionsToScheduleMigrationAsync(), + 7 => await serviceProvider.GetRequiredService().UploadNewCommerceMigrationSchedulesAsync(), + 8 => await serviceProvider.GetRequiredService().ExportNewCommerceMigrationSchedulesAsync(), + 9 => await serviceProvider.GetRequiredService().CancelNewCommerceMigrationSchedulesAsync(), _ => throw new InvalidOperationException("Invalid input") }; diff --git a/nce-bulk-migration-tool/NCEBulkMigrationTool/SubscriptionProvider.cs b/nce-bulk-migration-tool/NCEBulkMigrationTool/SubscriptionProvider.cs index 95e8565..c7fe5f2 100644 --- a/nce-bulk-migration-tool/NCEBulkMigrationTool/SubscriptionProvider.cs +++ b/nce-bulk-migration-tool/NCEBulkMigrationTool/SubscriptionProvider.cs @@ -40,7 +40,10 @@ internal class SubscriptionProvider : ISubscriptionProvider var authenticationResult = await this.tokenProvider.GetTokenAsync(); partnerTenantId = authenticationResult.TenantId; - var httpClient = new HttpClient(); + var httpClient = new HttpClient + { + BaseAddress = new Uri(Routes.BaseUrl) + }; httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken); httpClient.DefaultRequestHeaders.Add(Constants.PartnerCenterClientHeader, Constants.ClientName); @@ -98,7 +101,10 @@ internal class SubscriptionProvider : ISubscriptionProvider var authenticationResult = await this.tokenProvider.GetTokenAsync(); partnerTenantId = authenticationResult.TenantId; - var httpClient = new HttpClient(); + var httpClient = new HttpClient + { + BaseAddress = new Uri(Routes.BaseUrl) + }; httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken); httpClient.DefaultRequestHeaders.Add(Constants.PartnerCenterClientHeader, Constants.ClientName); @@ -182,7 +188,7 @@ internal class SubscriptionProvider : ISubscriptionProvider httpClient.DefaultRequestHeaders.Clear(); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken); httpClient.DefaultRequestHeaders.Add(Constants.PartnerCenterClientHeader, Constants.ClientName); - subscriptionRequest = new HttpRequestMessage(HttpMethod.Get, Routes.GetCustomers); + subscriptionRequest = new HttpRequestMessage(HttpMethod.Get, string.Format(Routes.GetSubscriptions, customer.TenantId)); subscriptionRequest.Headers.Add("MS-CorrelationId", Guid.NewGuid().ToString()); subscriptionResponse = await httpClient.SendAsync(subscriptionRequest).ConfigureAwait(false); @@ -229,7 +235,10 @@ internal class SubscriptionProvider : ISubscriptionProvider httpClient.DefaultRequestHeaders.Clear(); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken); httpClient.DefaultRequestHeaders.Add(Constants.PartnerCenterClientHeader, Constants.ClientName); - migrationRequest = new HttpRequestMessage(HttpMethod.Get, Routes.GetCustomers); + migrationRequest = new HttpRequestMessage(HttpMethod.Post, string.Format(Routes.ValidateMigrationEligibility, customer.TenantId)) + { + Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json") + }; migrationRequest.Headers.Add("MS-CorrelationId", Guid.NewGuid().ToString()); migrationResponse = await httpClient.SendAsync(migrationRequest).ConfigureAwait(false); @@ -237,7 +246,7 @@ internal class SubscriptionProvider : ISubscriptionProvider migrationResponse.EnsureSuccessStatusCode(); var newCommerceEligibility = await migrationResponse.Content.ReadFromJsonAsync().ConfigureAwait(false); - if (newCommerceEligibility!.AddOnMigrations.Any()) + if (newCommerceEligibility.AddOnMigrations.Any()) { addOnEligibilityList.Add(newCommerceEligibility.AddOnMigrations); } @@ -289,6 +298,7 @@ internal class SubscriptionProvider : ISubscriptionProvider Term = subscription.TermDuration, BillingPlan = subscription.BillingCycle.ToString(), SeatCount = subscription.Quantity, + CustomTermEndDate = newCommerceEligibility.CustomTermEndDate, AddOn = !string.IsNullOrWhiteSpace(subscription.ParentSubscriptionId), BaseSubscriptionId = subscription.ParentSubscriptionId, MigrationIneligibilityReason = newCommerceEligibility.Errors.Any() ?