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() ?