Skip to content

Commit

Permalink
Merge pull request #11 from sherweb/CreateTransfer
Browse files Browse the repository at this point in the history
CreateTransfer endpoint added
  • Loading branch information
asantossheweb authored Nov 21, 2024
2 parents 85456cb + 8e91a62 commit 8fd40c8
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 42 deletions.
3 changes: 2 additions & 1 deletion Xtkl.NceTransferWebhooks/DTOs/CreateTransferDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@ record CreateTransferDto
[SwaggerSchema("The email ID of the customer to receive notifications of the transfer creation.")]
public string CustomerEmailId { get; init; }

Check warning on line 27 in Xtkl.NceTransferWebhooks/DTOs/CreateTransferDto.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'CustomerEmailId' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

[JsonRequired]
[SwaggerSchema("The name of the customer whose subscriptions are being transferred.")]
public string CustomerName { get; init; }

Check warning on line 31 in Xtkl.NceTransferWebhooks/DTOs/CreateTransferDto.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'CustomerName' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

[SwaggerSchema("A GUID formatted partner tenant ID that identifies the partner to whom the transfer is targeted.")]
public Guid TargetPartnerTenantId { get; init; }
public Guid? TargetPartnerTenantId { get; init; }

[SwaggerSchema("The email ID of the partner to whom the transfer is targeted.")]
public string TargetPartnerEmailId { get; init; }

Check warning on line 37 in Xtkl.NceTransferWebhooks/DTOs/CreateTransferDto.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'TargetPartnerEmailId' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
Expand Down
10 changes: 10 additions & 0 deletions Xtkl.NceTransferWebhooks/DTOs/TokenResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Xtkl.NceTransferWebhooks.DTOs
{
public record TokenResponse(
string access_token,
string token_type,
string expires_on,
string scope,
string refresh_token
);
}
7 changes: 6 additions & 1 deletion Xtkl.NceTransferWebhooks/Enums.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@

enum TenantRegion
{
CA, // Canada
US, // United States
CA, // Canada
EU // Europe
}

Expand All @@ -11,3 +11,8 @@ enum TransferStatus
Complete,
Expired
}

enum TransferType
{
NewCommerce = 3
}
147 changes: 107 additions & 40 deletions Xtkl.NceTransferWebhooks/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@
using System.Text.Json.Serialization;
using Xtkl.NceTransferWebhooks.DTOs;
using Xtkl.NceTransferWebhooks.Model;
using Microsoft.Extensions.Caching.Memory;

var builder = WebApplication.CreateBuilder(args);

builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.SetMinimumLevel(LogLevel.Debug);

builder.Services.AddMemoryCache();

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
Expand All @@ -35,14 +38,20 @@

app.UseHttpsRedirection();

app.MapPost("/create-transfer", async (CreateTransferDto request, IConfiguration config) =>
app.MapPost("/create-transfer", async (CreateTransferDto request, IConfiguration config, IMemoryCache memoryCache) =>
{
var partnerCredentials = GetPartnerCredentials(request.TenantRegion, config);
if (request.CustomerId == Guid.Empty || request.SourcePartnerTenantId == Guid.Empty ||
string.IsNullOrEmpty(request.CustomerEmailId) || string.IsNullOrEmpty(request.SourcePartnerName) || string.IsNullOrEmpty(request.CustomerName))
{
return Results.Ok("'CustomerId', 'SourcePartnerTenantId', 'SourcePartnerName', 'CustomerName', and 'CustomerEmailId' are required.");
}

try
{
var partnerCredentials = await GetPartnerCredentials(request.TenantRegion, config, memoryCache);

var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", partnerCredentials.Credentials.PartnerServiceToken);
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", partnerCredentials.PartnerServiceToken);
httpClient.BaseAddress = new Uri(config["Transfer:PartnerCenterUrl"]);

Check warning on line 55 in Xtkl.NceTransferWebhooks/Program.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'uriString' in 'Uri.Uri(string uriString)'.

var TransferRequest = new
Expand All @@ -51,19 +60,19 @@
request.SourcePartnerName,
request.CustomerEmailId,
request.CustomerName,
//request.TargetPartnerTenantId,
//request.TargetPartnerEmailId,
TransferType = "3" // 3 represents NewCommerce and should be used for Azure plan and new commerce license-based subscriptions.
request.TargetPartnerTenantId,
request.TargetPartnerEmailId,
TransferType = TransferType.NewCommerce.GetHashCode()
};

var content = new StringContent(JsonSerializer.Serialize(TransferRequest), Encoding.UTF8, "application/json");

var response = await httpClient.PostAsync($"v1/customers/{request.CustomerId}/transfers", content);

return response.IsSuccessStatusCode
? Results.Ok("Request created successfully.")
? Results.Ok("Transfer created successfully.")
: Results.Problem(
detail: "Error sending data to external endpoint.",
detail: "Internal server error - unexpected error occurred",
statusCode: (int)response.StatusCode
);
}
Expand All @@ -77,17 +86,16 @@
summary: "Creates a new NCE transfer request",
description: "This endpoint creates a transfer request for a customer's subscription between partners."
))
.WithMetadata(new SwaggerResponseAttribute(200, "Request created successfully"))
.WithMetadata(new SwaggerResponseAttribute(400, "Bad request - invalid or missing data"))
.WithMetadata(new SwaggerResponseAttribute(404, "Not found - customer or partner ID does not exist"))
.WithMetadata(new SwaggerResponseAttribute(200, "Transfer created successfully"))
.WithMetadata(new SwaggerResponseAttribute(400, "'CustomerId', 'SourcePartnerTenantId', 'SourcePartnerName', 'CustomerName', and 'CustomerEmailId' are required"))
.WithMetadata(new SwaggerResponseAttribute(500, "Internal server error - unexpected error occurred"))
.WithOpenApi();

app.MapPost("/transfer-webhook-us", async (TransferWebhookDto request, IConfiguration config) =>
app.MapPost("/transfer-webhook-us", async (TransferWebhookDto request, IConfiguration config, IMemoryCache memoryCache) =>
{
try
{
var transfer = await GetTransfer(request.AuditUri, TenantRegion.US, config);
var transfer = await GetTransfer(request.AuditUri, TenantRegion.US, config, memoryCache);

await SendEmail(transfer, request.EventName, TenantRegion.US, config);

Expand All @@ -99,13 +107,20 @@
}
})
.WithName("TransferWebhookUsa")
.WithMetadata(new SwaggerOperationAttribute(
summary: "This event is raised when the transfer is complete",
description: "This endpoint receives notifications from the Microsoft Partner Center when an NCE (New Commerce Experience) transfer is completed or expires within the US tenant environment."
))
.WithMetadata(new SwaggerResponseAttribute(200, "Notification processed successfully"))
.WithMetadata(new SwaggerResponseAttribute(409, "The transfer is not in 'Complete' or 'Expired' status and cannot be processed."))
.WithMetadata(new SwaggerResponseAttribute(500, "Internal server error - unexpected error occurred"))
.WithOpenApi();

app.MapPost("/transfer-webhook-ca", async (TransferWebhookDto request, IConfiguration config) =>
app.MapPost("/transfer-webhook-ca", async (TransferWebhookDto request, IConfiguration config, IMemoryCache memoryCache) =>
{
try
{
var transfer = await GetTransfer(request.AuditUri, TenantRegion.CA, config);
var transfer = await GetTransfer(request.AuditUri, TenantRegion.CA, config, memoryCache);

await SendEmail(transfer, request.EventName, TenantRegion.CA, config);

Expand All @@ -117,18 +132,25 @@
}
})
.WithName("TransferWebhookCanada")
.WithMetadata(new SwaggerOperationAttribute(
summary: "This event is raised when the transfer is complete",
description: "This endpoint receives notifications from the Microsoft Partner Center when an NCE (New Commerce Experience) transfer is completed or expires within the CA tenant environment."
))
.WithMetadata(new SwaggerResponseAttribute(200, "Notification processed successfully"))
.WithMetadata(new SwaggerResponseAttribute(409, "The transfer is not in 'Complete' or 'Expired' status and cannot be processed."))
.WithMetadata(new SwaggerResponseAttribute(500, "Internal server error - unexpected error occurred"))
.WithOpenApi();


app.Run();

#region Private
async Task<Transfer> GetTransfer(string url, TenantRegion region, IConfiguration config)
async Task<Transfer> GetTransfer(string url, TenantRegion region, IConfiguration config, IMemoryCache memoryCache)
{
var partnerCredentials = GetPartnerCredentials(region, config);
var partnerCredentials = await GetPartnerCredentials(region, config, memoryCache);

var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", partnerCredentials.Credentials.PartnerServiceToken);
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", partnerCredentials.PartnerServiceToken);
httpClient.BaseAddress = new Uri(config["Transfer:PartnerCenterUrl"]);

Check warning on line 154 in Xtkl.NceTransferWebhooks/Program.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'uriString' in 'Uri.Uri(string uriString)'.

var parts = url.Split('_');
Expand All @@ -142,8 +164,7 @@ async Task<Transfer> GetTransfer(string url, TenantRegion region, IConfiguration

return JsonSerializer.Deserialize<Transfer>(result);

Check warning on line 165 in Xtkl.NceTransferWebhooks/Program.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference return.
}

async Task SendEmail(Transfer transfer, string status, TenantRegion region, IConfiguration config)
async Task SendEmail(Transfer transfer, string eventName, TenantRegion region, IConfiguration config)
{
var apiKey = config["SendGrid:ApiKey"];
var fromEmail = config["SendGrid:FromEmail"];
Expand All @@ -153,7 +174,7 @@ async Task SendEmail(Transfer transfer, string status, TenantRegion region, ICon

var client = new SendGridClient(apiKey);
var from = new EmailAddress(fromEmail, fromName);
var subject = $"NCE Transfer {status} - {region.ToString()}";
var subject = $"NCE Transfer {eventName} - {region.ToString()}";
var to = new EmailAddress(toEmail, toName);

var htmlContent = $@"
Expand All @@ -171,6 +192,7 @@ async Task SendEmail(Transfer transfer, string status, TenantRegion region, ICon
<li><strong>Created Time:</strong> {transfer.createdTime}</li>
<li><strong>Complete Time:</strong> {transfer.completedTime}</li>
<li><strong>Expired Time:</strong> {transfer.expirationTime}</li>
<li><strong>Status:</strong> {transfer.status}</li>
</ul>
<p>If you have any questions or need further assistance, please don’t hesitate to reach out.</p>
Expand All @@ -184,43 +206,88 @@ async Task SendEmail(Transfer transfer, string status, TenantRegion region, ICon

await client.SendEmailAsync(msg);
}
async Task<IResult> SendToCumulus(Transfer transfer, IConfiguration configuration)
{
if (!transfer.status.Equals(TransferStatus.Complete.ToString(), StringComparison.OrdinalIgnoreCase) &&
!transfer.status.Equals(TransferStatus.Expired.ToString(), StringComparison.OrdinalIgnoreCase))
{
return Results.Conflict("The transfer is not in 'Complete' or 'Expired' status and cannot be processed.");
}

var httpClient = new HttpClient();

var jsonContent = new StringContent(JsonSerializer.Serialize(transfer), Encoding.UTF8, "application/json");

var endpointUrl = configuration["Transfer:CumulusEndpoint"];

var httpResponse = await httpClient.PostAsync(endpointUrl, jsonContent);

IAggregatePartner GetPartnerCredentials(TenantRegion region, IConfiguration config)
return httpResponse.IsSuccessStatusCode
? Results.Ok("Notification processed successfully")
: Results.Problem("Internal server error - unexpected error occurred");
}
async Task<IPartnerCredentials> GetPartnerCredentials(TenantRegion region, IConfiguration config, IMemoryCache memoryCache)
{
var regionKey = region.ToString();

var clientId = config[$"Transfer:TenantRegion:{regionKey}:ClientId"];
var appSecret = config[$"Transfer:TenantRegion:{regionKey}:AppSecret"];
var appDomain = config[$"Transfer:TenantRegion:{regionKey}:AppDomain"];
var tenantId = config[$"Transfer:TenantRegion:{regionKey}:TenantId"];
var refreshToken = config[$"Transfer:TenantRegion:{regionKey}:RefreshToken"];

if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(appSecret) || string.IsNullOrEmpty(appDomain))
var loginUrl = config[$"Transfer:ADDLoginUrl"];
var scope = config["Transfer:Scope"];

if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(appSecret) || string.IsNullOrEmpty(tenantId) || string.IsNullOrEmpty(refreshToken))
{
throw new InvalidOperationException($"Missing configuration for region '{regionKey}'. Ensure that all credentials are provided.");
}

IPartnerCredentials partnerCredentials = PartnerCredentials.Instance.GenerateByApplicationCredentials(clientId, appSecret, appDomain);
var cacheKey = $"AuthToken_{regionKey}";

return PartnerService.Instance.CreatePartnerOperations(partnerCredentials);
}

async Task<IResult> SendToCumulus(Transfer transfer, IConfiguration configuration)
{
if (!transfer.status.Equals(TransferStatus.Complete.ToString(), StringComparison.OrdinalIgnoreCase) &&
!transfer.status.Equals(TransferStatus.Expired.ToString(), StringComparison.OrdinalIgnoreCase))
if (memoryCache.TryGetValue<AuthenticationToken>(cacheKey, out var cachedAuthToken) &&
cachedAuthToken.ExpiryTime > DateTimeOffset.UtcNow)

Check warning on line 249 in Xtkl.NceTransferWebhooks/Program.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
{
return Results.Conflict("The transfer is not in 'Complete' or 'Expired' status and cannot be processed.");
return await PartnerCredentials.Instance.GenerateByUserCredentialsAsync(clientId, cachedAuthToken);
}

var httpClient = new HttpClient();
var postData = new Dictionary<string, string>
{
{ "client_id", clientId },
{ "scope", scope },
{ "refresh_token", refreshToken },
{ "grant_type", "refresh_token" },
{ "client_secret", appSecret }
};

var jsonContent = new StringContent(JsonSerializer.Serialize(transfer), Encoding.UTF8, "application/json");
var url = $"{loginUrl}/{tenantId}/oauth2/token";
TokenResponse tokenResponse = null;

var endpointUrl = configuration["Transfer:CumulusEndpoint"];
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-www-form-urlencoded"));

var httpResponse = await httpClient.PostAsync(endpointUrl, jsonContent);
var content = new FormUrlEncodedContent(postData);
var response = await client.PostAsync(url, content);

return httpResponse.IsSuccessStatusCode
? Results.Ok("Notification sent successfully.")
: Results.Problem("Error sending data to external endpoint.");
if (!response.IsSuccessStatusCode)
{
throw new InvalidOperationException("Error authenticating user.");
}

var responseContent = await response.Content.ReadAsStringAsync();
tokenResponse = JsonSerializer.Deserialize<TokenResponse>(responseContent)
?? throw new InvalidOperationException("Failed to deserialize token response.");
}

var authToken = new AuthenticationToken(
tokenResponse.access_token,
DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64(tokenResponse.expires_on))
);

memoryCache.Set(cacheKey, authToken);

return await PartnerCredentials.Instance.GenerateByUserCredentialsAsync(clientId, authToken);
}
#endregion

36 changes: 36 additions & 0 deletions Xtkl.NceTransferWebhooks/appsettings.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,41 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"SendGrid": {
"ApiKey": "",
"FromEmail": "",
"FromName": "Sherweb Team",
"ToEmail": "",
"ToName": ""
},
"Transfer": {
"ADDLoginUrl": "",
"CumulusEndpoint": "",
"PartnerCenterUrl": "",
"Scope": "",
"TenantRegion": {
"CA": {
"TenantId": "ClientId",
"ClientId": "ClientId",
"AppSecret": "AppSecret",
"AppDomain": "AppDomain",
"RefreshToken": "ClientId"
},
"EU": {
"TenantId": "ClientId",
"ClientId": "ClientId",
"AppSecret": "AppSecret",
"AppDomain": "AppDomain",
"RefreshToken": "ClientId"
},
"US": {
"TenantId": "ClientId",
"ClientId": "ClientId",
"AppSecret": "AppSecret",
"AppDomain": "AppDomain",
"RefreshToken": "ClientId"
}
}
}
}

0 comments on commit 8fd40c8

Please sign in to comment.