From 9d8fedccc7d9a4682555bd73e3146ffe5ddb557c Mon Sep 17 00:00:00 2001 From: "neha@quantoz" Date: Thu, 2 Nov 2023 12:18:06 +0100 Subject: [PATCH 1/7] Draft --- .../Entities/MailAggregate/Mail.cs | 54 +++++++++++++ .../Repositories/IMailsRepository.cs | 13 ++++ .../Jobs/ProcessEmailsJob.cs | 76 +++++++++++++++++++ .../src/Core.Infrastructure/Nexus/Enums.cs | 5 ++ .../Repositories/NexusMailsRepository.cs | 62 +++++++++++++++ 5 files changed, 210 insertions(+) create mode 100644 backend/core/src/Core.Domain/Entities/MailAggregate/Mail.cs create mode 100644 backend/core/src/Core.Domain/Repositories/IMailsRepository.cs create mode 100644 backend/core/src/Core.Infrastructure/Jobs/ProcessEmailsJob.cs create mode 100644 backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusMailsRepository.cs diff --git a/backend/core/src/Core.Domain/Entities/MailAggregate/Mail.cs b/backend/core/src/Core.Domain/Entities/MailAggregate/Mail.cs new file mode 100644 index 00000000..8bcc8993 --- /dev/null +++ b/backend/core/src/Core.Domain/Entities/MailAggregate/Mail.cs @@ -0,0 +1,54 @@ +// Copyright 2023 Quantoz Technology B.V. and contributors. Licensed +// under the Apache License, Version 2.0. See the NOTICE file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +namespace Core.Domain.Entities.MailAggregate +{ + public class Mail + { + public required string Code { get; set; } + + public string? Created { get; set; } + + public string? Sent { get; set; } + + public string? Status { get; set; } + + public string? Type { get; set; } + + public int? Count { get; set; } + + public MailEntityCodes? References { get; set; } + + public MailContent? Content { get; set; } + + public MailRecipient? Recipient { get; set; } + } + + public class MailEntityCodes + { + public string? AccountCode { get; set; } + + public string? CustomerCode { get; set; } + + public string? TransactionCode { get; set; } + } + + public class MailContent + { + public string? Subject { get; set; } + + public string? Html { get; set; } + + public string? Text { get; set; } + } + + public class MailRecipient + { + public string? Email { get; set; } + + public string? CC { get; set; } + + public string? BCC { get; set; } + } +} diff --git a/backend/core/src/Core.Domain/Repositories/IMailsRepository.cs b/backend/core/src/Core.Domain/Repositories/IMailsRepository.cs new file mode 100644 index 00000000..1ef06b7f --- /dev/null +++ b/backend/core/src/Core.Domain/Repositories/IMailsRepository.cs @@ -0,0 +1,13 @@ +// Copyright 2023 Quantoz Technology B.V. and contributors. Licensed +// under the Apache License, Version 2.0. See the NOTICE file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +using Core.Domain.Entities.MailAggregate; + +namespace Core.Domain.Repositories +{ + public interface IMailsRepository + { + Task GetMailsAsync(string name, CancellationToken cancellationToken = default); + } +} diff --git a/backend/core/src/Core.Infrastructure/Jobs/ProcessEmailsJob.cs b/backend/core/src/Core.Infrastructure/Jobs/ProcessEmailsJob.cs new file mode 100644 index 00000000..64e444e0 --- /dev/null +++ b/backend/core/src/Core.Infrastructure/Jobs/ProcessEmailsJob.cs @@ -0,0 +1,76 @@ +// Copyright 2023 Quantoz Technology B.V. and contributors. Licensed +// under the Apache License, Version 2.0. See the NOTICE file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +using Core.Domain; +using Core.Domain.Repositories; +using Microsoft.Extensions.Logging; +using Quartz; +using System.Net.Http.Json; + +namespace Core.Infrastructure.Jobs +{ + public class ProcessEmailsJob : IJob + { + private readonly HttpClient _client; + private readonly ILogger _logger; + private readonly ICallbackRepository _callbackRepository; + private readonly IUnitOfWork _unitOfWork; + + public ProcessEmailsJob(HttpClient client, + ILogger logger, + ICallbackRepository callbackRepository, + IUnitOfWork unitOfWork) + { + _client = client; + _logger = logger; + _callbackRepository = callbackRepository; + _unitOfWork = unitOfWork; + } + + public async Task Execute(IJobExecutionContext context) + { + var callbacks = await _callbackRepository.GetLatestCreatedAsync(context.CancellationToken); + + if (callbacks.Any()) + { + _logger.LogInformation("Sending callbacks"); + + foreach (var callback in callbacks) + { + var body = new + { + callback.Code, + callback.Content, + CreatedOn = DateTimeProvider.ToUnixTimeInMilliseconds(callback.CreatedOn), + Type = callback.Type.ToString() + }; + + try + { + var response = await _client.PostAsJsonAsync(callback.DestinationUrl, body); + + if (response.IsSuccessStatusCode) + { + callback.Sent(); + } + else + { + callback.Failed(); + } + } + catch (Exception ex) + { + _logger.LogError("An error occured sending callback {code} with message {message}", callback.Code, ex.Message); + callback.Failed(); + } + + + _callbackRepository.Update(callback); + } + + await _unitOfWork.SaveChangesAsync(); + } + } + } +} diff --git a/backend/core/src/Core.Infrastructure/Nexus/Enums.cs b/backend/core/src/Core.Infrastructure/Nexus/Enums.cs index 463e2690..90a41f80 100644 --- a/backend/core/src/Core.Infrastructure/Nexus/Enums.cs +++ b/backend/core/src/Core.Infrastructure/Nexus/Enums.cs @@ -26,4 +26,9 @@ public enum NexusErrorCodes InvalidProperty = 5, TransactionNotFoundError = 6 } + + public enum MailStatus + { + ReadyToSend = 1 + } } diff --git a/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusMailsRepository.cs b/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusMailsRepository.cs new file mode 100644 index 00000000..69f195fd --- /dev/null +++ b/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusMailsRepository.cs @@ -0,0 +1,62 @@ +// Copyright 2023 Quantoz Technology B.V. and contributors. Licensed +// under the Apache License, Version 2.0. See the NOTICE file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +using Core.Domain.Entities.MailAggregate; +using Core.Domain.Entities.SettingsAggregate; +using Core.Domain.Exceptions; +using Core.Domain.Repositories; +using Nexus.Token.SDK; + +namespace Core.Infrastructure.Nexus.Repositories +{ + public class NexusMailsRepository : IMailsRepository + { + private readonly ITokenServer _tokenServer; + + public NexusMailsRepository(ITokenServer tokenServer) + { + _tokenServer = tokenServer; + } + + // public async Task> GetMailsAsync(string name, CancellationToken cancellationToken = default) + // { + // return null; + //// var query = new Dictionary() + //// { + //// { "status", MailStatus.ReadyToSend.ToString()}, + //// }; + + //// var response = await _tokenServer.Compliance.Mails.Get(query); + + //// var mails = response.Records; + + //// var items = new List(); + + //// return mails + ////.Select(b => new AccountBalance + ////{ + //// Balance = b.Amount, + //// TokenCode = b.TokenCode + ////}); + + //// return new TrustLevel + //// { + //// FundingMothly = trustlevel.BuyLimits?.MonthlyLimit, + //// WithdrawMonthly = trustlevel.SellLimits?.MonthlyLimit, + //// Name = trustlevel.Name, + //// Description = trustlevel.Description + //// }; + // } + + public Task GetMailsAsync(string name, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + return Task.FromResult(true); + } + } +} From d17046b9e7ff77523f42dad0c07c96c6385c541f Mon Sep 17 00:00:00 2001 From: "neha@quantoz" Date: Thu, 9 Nov 2023 12:13:28 +0100 Subject: [PATCH 2/7] Initial draft --- .../InfrastructureInjection.cs | 1 + .../Abstractions/ISendGridMailService.cs | 10 ++ .../TransactionAggregate/Transaction.cs | 2 +- .../Repositories/IMailsRepository.cs | 2 +- .../Repositories/ITransactionRepository.cs | 2 + .../SendGridMailService.cs | 64 ++++++++++ .../SendGridMailServiceOptions.cs | 29 +++++ .../Core.Infrastructure.csproj | 1 + .../Jobs/ProcessEmailsJob.cs | 112 ++++++++++++------ .../src/Core.Infrastructure/Nexus/Enums.cs | 5 + .../Repositories/NexusMailsRepository.cs | 90 ++++++++------ .../NexusTransactionRepository.cs | 18 +++ 12 files changed, 264 insertions(+), 72 deletions(-) create mode 100644 backend/core/src/Core.Domain/Abstractions/ISendGridMailService.cs create mode 100644 backend/core/src/Core.Infrastructure/Compliance/SendGridMailService/SendGridMailService.cs create mode 100644 backend/core/src/Core.Infrastructure/Compliance/SendGridMailService/SendGridMailServiceOptions.cs diff --git a/backend/core/src/Core.API/DependencyInjection/InfrastructureInjection.cs b/backend/core/src/Core.API/DependencyInjection/InfrastructureInjection.cs index 375c8ccb..382732f2 100644 --- a/backend/core/src/Core.API/DependencyInjection/InfrastructureInjection.cs +++ b/backend/core/src/Core.API/DependencyInjection/InfrastructureInjection.cs @@ -51,6 +51,7 @@ private static IServiceCollection AddNexus(this IServiceCollection services, ICo services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } diff --git a/backend/core/src/Core.Domain/Abstractions/ISendGridMailService.cs b/backend/core/src/Core.Domain/Abstractions/ISendGridMailService.cs new file mode 100644 index 00000000..26518cad --- /dev/null +++ b/backend/core/src/Core.Domain/Abstractions/ISendGridMailService.cs @@ -0,0 +1,10 @@ +// Copyright 2023 Quantoz Technology B.V. and contributors. Licensed +// under the Apache License, Version 2.0. See the NOTICE file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +namespace Core.Domain.Abstractions +{ + public interface ISendGridMailService + { + } +} \ No newline at end of file diff --git a/backend/core/src/Core.Domain/Entities/TransactionAggregate/Transaction.cs b/backend/core/src/Core.Domain/Entities/TransactionAggregate/Transaction.cs index 371cb962..2d958eb8 100644 --- a/backend/core/src/Core.Domain/Entities/TransactionAggregate/Transaction.cs +++ b/backend/core/src/Core.Domain/Entities/TransactionAggregate/Transaction.cs @@ -24,7 +24,7 @@ public class Transaction public string? Memo { get; set; } - public required string Direction { get; set; } + public string? Direction { get; set; } public Payment? Payment { get; set; } diff --git a/backend/core/src/Core.Domain/Repositories/IMailsRepository.cs b/backend/core/src/Core.Domain/Repositories/IMailsRepository.cs index 1ef06b7f..2cdedbd7 100644 --- a/backend/core/src/Core.Domain/Repositories/IMailsRepository.cs +++ b/backend/core/src/Core.Domain/Repositories/IMailsRepository.cs @@ -8,6 +8,6 @@ namespace Core.Domain.Repositories { public interface IMailsRepository { - Task GetMailsAsync(string name, CancellationToken cancellationToken = default); + Task> GetMailsAsync(string status, CancellationToken cancellationToken = default); } } diff --git a/backend/core/src/Core.Domain/Repositories/ITransactionRepository.cs b/backend/core/src/Core.Domain/Repositories/ITransactionRepository.cs index 5c8ae1d9..6942ace7 100644 --- a/backend/core/src/Core.Domain/Repositories/ITransactionRepository.cs +++ b/backend/core/src/Core.Domain/Repositories/ITransactionRepository.cs @@ -15,6 +15,8 @@ public interface ITransactionRepository public Task> GetAsync(string publicKey, int page, int pageSize, CancellationToken cancellationToken = default); + public Task GetByCodeAsync(string code, CancellationToken cancellationToken = default); + public Task GetWithdrawFeesAsync(Withdraw withdraw, CancellationToken cancellationToken = default); } } diff --git a/backend/core/src/Core.Infrastructure/Compliance/SendGridMailService/SendGridMailService.cs b/backend/core/src/Core.Infrastructure/Compliance/SendGridMailService/SendGridMailService.cs new file mode 100644 index 00000000..695f8b39 --- /dev/null +++ b/backend/core/src/Core.Infrastructure/Compliance/SendGridMailService/SendGridMailService.cs @@ -0,0 +1,64 @@ +// Copyright 2023 Quantoz Technology B.V. and contributors. Licensed +// under the Apache License, Version 2.0. See the NOTICE file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +using Core.Domain.Abstractions; +using Core.Domain.Entities.MailAggregate; +using Core.Infrastructure.Nexus; +using SendGrid; +using SendGrid.Helpers.Mail; +using System.Net; + +namespace Core.Infrastructure.Compliance.IPLocator +{ + public class SendGridMailService : ISendGridMailService + { + private readonly HttpClient _httpClient; + private readonly SendGridClient _sendGridClient; + private readonly SendGridMailServiceOptions _mailOptions; + + public SendGridMailService( + HttpClient httpClient, + SendGridClient sendGridClient, + SendGridMailServiceOptions mailOptions) + { + _mailOptions = mailOptions; + _httpClient = httpClient; + _sendGridClient = new SendGridClient(_mailOptions.ApiKey); + } + + public async Task SendMailAsync(Mail mail, string customerName, decimal amount) + { + if (mail == null) + { + var from = new EmailAddress(_mailOptions.Sender); + var to = new EmailAddress(mail.Recipient.Email); + var subject = mail.Content.Subject; + var htmlContent = mail.Content.Html; + var plainTextContent = mail.Content.Text; + + var msg = MailHelper.CreateSingleEmail(from, to, subject, plainTextContent, htmlContent); + + // Set the appropriate template ID based on the mail type + if (mail.Type == MailType.TransactionSellFinish.ToString()) + { + msg.SetTemplateId(_mailOptions.Templates.WithdrawalTemplateID); + + // Fill in the dynamic template fields + msg.AddSubstitution("{{ customerFullName }}", customerName); + msg.AddSubstitution("{{ amount }}", amount.ToString()); + msg.AddSubstitution("{{ accountCode }}", mail.References.AccountCode); + + + } + + var response = await _sendGridClient.SendEmailAsync(msg); + + if (response.StatusCode != HttpStatusCode.Accepted) + { + // Handle failure to send email + } + } + } + } +} diff --git a/backend/core/src/Core.Infrastructure/Compliance/SendGridMailService/SendGridMailServiceOptions.cs b/backend/core/src/Core.Infrastructure/Compliance/SendGridMailService/SendGridMailServiceOptions.cs new file mode 100644 index 00000000..8f5b9911 --- /dev/null +++ b/backend/core/src/Core.Infrastructure/Compliance/SendGridMailService/SendGridMailServiceOptions.cs @@ -0,0 +1,29 @@ +// Copyright 2023 Quantoz Technology B.V. and contributors. Licensed +// under the Apache License, Version 2.0. See the NOTICE file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +using System.ComponentModel.DataAnnotations; + +namespace Core.Infrastructure.Compliance.IPLocator +{ + public class SendGridMailServiceOptions + { + [Required] + public required string ApiKey { get; set; } + + [Required] + public required Templates Templates { get; set; } + + [Required] + public required string Sender { get; set; } + } + + public class Templates + { + [Required] + public required string WithdrawalTemplateID { get; set; } + + [Required] + public required string FundingtemplateID { get; set; } + } +} diff --git a/backend/core/src/Core.Infrastructure/Core.Infrastructure.csproj b/backend/core/src/Core.Infrastructure/Core.Infrastructure.csproj index 802e0a6f..ffaf9578 100644 --- a/backend/core/src/Core.Infrastructure/Core.Infrastructure.csproj +++ b/backend/core/src/Core.Infrastructure/Core.Infrastructure.csproj @@ -11,6 +11,7 @@ + diff --git a/backend/core/src/Core.Infrastructure/Jobs/ProcessEmailsJob.cs b/backend/core/src/Core.Infrastructure/Jobs/ProcessEmailsJob.cs index 64e444e0..715bd847 100644 --- a/backend/core/src/Core.Infrastructure/Jobs/ProcessEmailsJob.cs +++ b/backend/core/src/Core.Infrastructure/Jobs/ProcessEmailsJob.cs @@ -4,6 +4,7 @@ using Core.Domain; using Core.Domain.Repositories; +using Core.Infrastructure.Nexus; using Microsoft.Extensions.Logging; using Quartz; using System.Net.Http.Json; @@ -14,63 +15,108 @@ public class ProcessEmailsJob : IJob { private readonly HttpClient _client; private readonly ILogger _logger; - private readonly ICallbackRepository _callbackRepository; + private readonly IMailsRepository _mailsRepository; + private readonly ICustomerRepository _customerRepository; + private readonly ITransactionRepository _transactionRepository; private readonly IUnitOfWork _unitOfWork; public ProcessEmailsJob(HttpClient client, ILogger logger, - ICallbackRepository callbackRepository, + IMailsRepository mailsRepository, + ICustomerRepository customerRepository, + ITransactionRepository transactionRepository, IUnitOfWork unitOfWork) { _client = client; _logger = logger; - _callbackRepository = callbackRepository; + _mailsRepository = mailsRepository; + _customerRepository = customerRepository; + _transactionRepository = transactionRepository; _unitOfWork = unitOfWork; } public async Task Execute(IJobExecutionContext context) { - var callbacks = await _callbackRepository.GetLatestCreatedAsync(context.CancellationToken); + var mails = _mailsRepository.GetMailsAsync(MailStatus.ReadyToSend.ToString(), context.CancellationToken).Result; - if (callbacks.Any()) + if (mails.Any()) { - _logger.LogInformation("Sending callbacks"); - - foreach (var callback in callbacks) + // Integrate sendgrid to send emails + foreach (var mail in mails) { - var body = new - { - callback.Code, - callback.Content, - CreatedOn = DateTimeProvider.ToUnixTimeInMilliseconds(callback.CreatedOn), - Type = callback.Type.ToString() - }; + // call customer repo for customer name + string customerName = string.Empty; + var customerCode = mail.References.CustomerCode; - try - { - var response = await _client.PostAsJsonAsync(callback.DestinationUrl, body); - - if (response.IsSuccessStatusCode) - { - callback.Sent(); - } - else - { - callback.Failed(); - } - } - catch (Exception ex) + var customer = await _customerRepository.GetAsync(customerCode, context.CancellationToken); + + + + if (customer != null) { - _logger.LogError("An error occured sending callback {code} with message {message}", callback.Code, ex.Message); - callback.Failed(); + customerName = customer.GetName(); } + // call transaction repo for tx details + var transaction = await _transactionRepository.GetByCodeAsync(mail.References.TransactionCode, context.CancellationToken); + + + + + // call mail service also pass customerName, tx amount + - _callbackRepository.Update(callback); + //// Check the MailType before sending email + //// For Payout, the MailType should be TransactionSellFinish + //if (mail.Type == MailType.TransactionSellFinish.ToString()) + //{ + // // This is for Payout, so use payout template on sendgrid + //} } + } + + + + + //if (callbacks.Any()) + //{ + // _logger.LogInformation("Sending callbacks"); + + // foreach (var callback in callbacks) + // { + // var body = new + // { + // callback.Code, + // callback.Content, + // CreatedOn = DateTimeProvider.ToUnixTimeInMilliseconds(callback.CreatedOn), + // Type = callback.Type.ToString() + // }; + + // try + // { + // var response = await _client.PostAsJsonAsync(callback.DestinationUrl, body); + + // if (response.IsSuccessStatusCode) + // { + // callback.Sent(); + // } + // else + // { + // callback.Failed(); + // } + // } + // catch (Exception ex) + // { + // _logger.LogError("An error occured sending callback {code} with message {message}", callback.Code, ex.Message); + // callback.Failed(); + // } + + + // _callbackRepository.Update(callback); + // } await _unitOfWork.SaveChangesAsync(); } } } -} + diff --git a/backend/core/src/Core.Infrastructure/Nexus/Enums.cs b/backend/core/src/Core.Infrastructure/Nexus/Enums.cs index 90a41f80..efedb9e1 100644 --- a/backend/core/src/Core.Infrastructure/Nexus/Enums.cs +++ b/backend/core/src/Core.Infrastructure/Nexus/Enums.cs @@ -31,4 +31,9 @@ public enum MailStatus { ReadyToSend = 1 } + + public enum MailType + { + TransactionSellFinish = 1 + } } diff --git a/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusMailsRepository.cs b/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusMailsRepository.cs index 69f195fd..be487bcb 100644 --- a/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusMailsRepository.cs +++ b/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusMailsRepository.cs @@ -3,10 +3,9 @@ // of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 using Core.Domain.Entities.MailAggregate; -using Core.Domain.Entities.SettingsAggregate; -using Core.Domain.Exceptions; using Core.Domain.Repositories; -using Nexus.Token.SDK; +using Nexus.Sdk.Shared.Responses; +using Nexus.Sdk.Token; namespace Core.Infrastructure.Nexus.Repositories { @@ -19,44 +18,61 @@ public NexusMailsRepository(ITokenServer tokenServer) _tokenServer = tokenServer; } - // public async Task> GetMailsAsync(string name, CancellationToken cancellationToken = default) - // { - // return null; - //// var query = new Dictionary() - //// { - //// { "status", MailStatus.ReadyToSend.ToString()}, - //// }; - - //// var response = await _tokenServer.Compliance.Mails.Get(query); - - //// var mails = response.Records; - - //// var items = new List(); - - //// return mails - ////.Select(b => new AccountBalance - ////{ - //// Balance = b.Amount, - //// TokenCode = b.TokenCode - ////}); - - //// return new TrustLevel - //// { - //// FundingMothly = trustlevel.BuyLimits?.MonthlyLimit, - //// WithdrawMonthly = trustlevel.SellLimits?.MonthlyLimit, - //// Name = trustlevel.Name, - //// Description = trustlevel.Description - //// }; - // } - - public Task GetMailsAsync(string name, CancellationToken cancellationToken = default) + public async Task> GetMailsAsync(string status, CancellationToken cancellationToken = default) { - throw new NotImplementedException(); + var query = new Dictionary() + { + { + "status", status + } + }; + + var response = await _tokenServer.Compliance.Mails.Get(query); + + var mails = response.Records; + + var items = new List(); + + foreach (var mail in mails) + { + var item = ConvertToMailAsync(mail, cancellationToken); + items.Add(item); + } + + return items; } - public Task SaveChangesAsync(CancellationToken cancellationToken = default) + private static Mail ConvertToMailAsync(MailsResponse mail, CancellationToken cancellationToken = default) { - return Task.FromResult(true); + var response = new Mail + { + Code = mail.Code, + Type = mail.Type, + Status = mail.Status, + Content = new Domain.Entities.MailAggregate.MailContent + { + Html = mail.Content.Html, + Subject = mail.Content.Subject, + Text = mail.Content.Text + }, + Count = mail.Count, + Created = mail.Created, + Recipient = new Domain.Entities.MailAggregate.MailRecipient + { + BCC = mail.Recipient.BCC, + CC = mail.Recipient.CC, + Email = mail.Recipient.Email + }, + References = new Domain.Entities.MailAggregate.MailEntityCodes + { + AccountCode = mail.References.AccountCode, + CustomerCode = mail.References.CustomerCode, + TransactionCode = mail.References.TransactionCode + }, + Sent = mail.Sent + }; + + return response; } } } diff --git a/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusTransactionRepository.cs b/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusTransactionRepository.cs index c594a243..5aebb6c8 100644 --- a/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusTransactionRepository.cs +++ b/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusTransactionRepository.cs @@ -109,6 +109,24 @@ public async Task> GetAsync(string publicKey, int page, int p }; } + public async Task GetByCodeAsync(string transactionCode, CancellationToken cancellationToken = default) + { + var response = await _tokenServer.Operations.Get(transactionCode); + + return new Transaction() + { + TransactionCode = response.Code, + Amount = response.Amount, + Created = DateTimeOffset.Parse(response.Created), + Status = response.Status, + TokenCode = response.TokenCode, + Type = response.Type, + ToAccountCode = response.ReceiverAccount?.AccountCode, + FromAccountCode = response.SenderAccount?.AccountCode, + Memo = response?.Memo + }; + } + #region private methods private async Task CreateAlgorandPayment(Payment payment, string? ip = null) From b1f6f134f3c15f1d589e596630a742b6a43aacfd Mon Sep 17 00:00:00 2001 From: "neha@quantoz" Date: Thu, 16 Nov 2023 12:53:47 +0100 Subject: [PATCH 3/7] Added updating mail status to Sent --- .../Abstractions/ISendGridMailService.cs | 4 + .../Repositories/IMailsRepository.cs | 2 + .../Repositories/ITransactionRepository.cs | 2 +- .../SendGridMailService.cs | 62 +++++++------ .../Jobs/ProcessEmailsJob.cs | 92 ++++++------------- .../Repositories/NexusMailsRepository.cs | 27 ++++-- .../NexusTransactionRepository.cs | 41 ++++++--- 7 files changed, 119 insertions(+), 111 deletions(-) diff --git a/backend/core/src/Core.Domain/Abstractions/ISendGridMailService.cs b/backend/core/src/Core.Domain/Abstractions/ISendGridMailService.cs index 26518cad..79ee3a50 100644 --- a/backend/core/src/Core.Domain/Abstractions/ISendGridMailService.cs +++ b/backend/core/src/Core.Domain/Abstractions/ISendGridMailService.cs @@ -2,9 +2,13 @@ // under the Apache License, Version 2.0. See the NOTICE file at the root // of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 +using Core.Domain.Entities.CustomerAggregate; +using Core.Domain.Entities.MailAggregate; + namespace Core.Domain.Abstractions { public interface ISendGridMailService { + public Task SendMailAsync(Mail mail, Customer customer, decimal amount); } } \ No newline at end of file diff --git a/backend/core/src/Core.Domain/Repositories/IMailsRepository.cs b/backend/core/src/Core.Domain/Repositories/IMailsRepository.cs index 2cdedbd7..e1a57690 100644 --- a/backend/core/src/Core.Domain/Repositories/IMailsRepository.cs +++ b/backend/core/src/Core.Domain/Repositories/IMailsRepository.cs @@ -9,5 +9,7 @@ namespace Core.Domain.Repositories public interface IMailsRepository { Task> GetMailsAsync(string status, CancellationToken cancellationToken = default); + + Task UpdateMailSent(string code, CancellationToken cancellationToken = default); } } diff --git a/backend/core/src/Core.Domain/Repositories/ITransactionRepository.cs b/backend/core/src/Core.Domain/Repositories/ITransactionRepository.cs index 6942ace7..38717255 100644 --- a/backend/core/src/Core.Domain/Repositories/ITransactionRepository.cs +++ b/backend/core/src/Core.Domain/Repositories/ITransactionRepository.cs @@ -15,7 +15,7 @@ public interface ITransactionRepository public Task> GetAsync(string publicKey, int page, int pageSize, CancellationToken cancellationToken = default); - public Task GetByCodeAsync(string code, CancellationToken cancellationToken = default); + public Task GetByCodeAsync(string code, CancellationToken cancellationToken = default); public Task GetWithdrawFeesAsync(Withdraw withdraw, CancellationToken cancellationToken = default); } diff --git a/backend/core/src/Core.Infrastructure/Compliance/SendGridMailService/SendGridMailService.cs b/backend/core/src/Core.Infrastructure/Compliance/SendGridMailService/SendGridMailService.cs index 695f8b39..dc9388d6 100644 --- a/backend/core/src/Core.Infrastructure/Compliance/SendGridMailService/SendGridMailService.cs +++ b/backend/core/src/Core.Infrastructure/Compliance/SendGridMailService/SendGridMailService.cs @@ -3,7 +3,9 @@ // of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 using Core.Domain.Abstractions; +using Core.Domain.Entities.CustomerAggregate; using Core.Domain.Entities.MailAggregate; +using Core.Domain.Exceptions; using Core.Infrastructure.Nexus; using SendGrid; using SendGrid.Helpers.Mail; @@ -13,51 +15,59 @@ namespace Core.Infrastructure.Compliance.IPLocator { public class SendGridMailService : ISendGridMailService { - private readonly HttpClient _httpClient; private readonly SendGridClient _sendGridClient; private readonly SendGridMailServiceOptions _mailOptions; public SendGridMailService( - HttpClient httpClient, - SendGridClient sendGridClient, SendGridMailServiceOptions mailOptions) { _mailOptions = mailOptions; - _httpClient = httpClient; _sendGridClient = new SendGridClient(_mailOptions.ApiKey); } - public async Task SendMailAsync(Mail mail, string customerName, decimal amount) + public async Task SendMailAsync(Mail mail, Customer customer, decimal amount) { if (mail == null) { - var from = new EmailAddress(_mailOptions.Sender); - var to = new EmailAddress(mail.Recipient.Email); - var subject = mail.Content.Subject; - var htmlContent = mail.Content.Html; - var plainTextContent = mail.Content.Text; - - var msg = MailHelper.CreateSingleEmail(from, to, subject, plainTextContent, htmlContent); + throw new CustomErrorsException("MailService", "mail", "An error occured while sending mail."); + } - // Set the appropriate template ID based on the mail type - if (mail.Type == MailType.TransactionSellFinish.ToString()) - { - msg.SetTemplateId(_mailOptions.Templates.WithdrawalTemplateID); + var from = new EmailAddress(_mailOptions.Sender); + var to = new EmailAddress(mail.Recipient?.Email); + var subject = mail.Content?.Subject; + var htmlContent = mail.Content?.Html; + var plainTextContent = mail.Content?.Text; - // Fill in the dynamic template fields - msg.AddSubstitution("{{ customerFullName }}", customerName); - msg.AddSubstitution("{{ amount }}", amount.ToString()); - msg.AddSubstitution("{{ accountCode }}", mail.References.AccountCode); + if (to == null) + { + throw new CustomErrorsException("MailService", "toAddress", "An error occured while sending mail."); + } + var msg = MailHelper.CreateSingleEmail(from, to, subject, plainTextContent, htmlContent); - } + // If mail types is TransactionSellFinish which is Payout + if (mail.Type == MailType.TransactionSellFinish.ToString()) + { + msg.SetTemplateId(_mailOptions.Templates.WithdrawalTemplateID); + } + else // convert to else if + { + msg.SetTemplateId(_mailOptions.Templates.FundingtemplateID); + } - var response = await _sendGridClient.SendEmailAsync(msg); + // Fill in the dynamic template fields + msg.AddSubstitution("{{ customerFullName }}", customer?.GetName()); + msg.AddSubstitution("{{ amount }}", amount.ToString()); + msg.AddSubstitution("{{ accountCode }}", mail.References?.AccountCode); + msg.AddSubstitution("{{ customerBankAccount }}", customer?.BankAccount); + msg.AddSubstitution("{{ transactionCode }}", mail.References?.TransactionCode); + msg.AddSubstitution("{{ createdDate }}", mail.Created); - if (response.StatusCode != HttpStatusCode.Accepted) - { - // Handle failure to send email - } + var response = await _sendGridClient.SendEmailAsync(msg); + + if (response.StatusCode != HttpStatusCode.Accepted) + { + throw new CustomErrorsException("MailService", "mail", "An error occured while sending mail."); } } } diff --git a/backend/core/src/Core.Infrastructure/Jobs/ProcessEmailsJob.cs b/backend/core/src/Core.Infrastructure/Jobs/ProcessEmailsJob.cs index 715bd847..33432f22 100644 --- a/backend/core/src/Core.Infrastructure/Jobs/ProcessEmailsJob.cs +++ b/backend/core/src/Core.Infrastructure/Jobs/ProcessEmailsJob.cs @@ -2,12 +2,12 @@ // under the Apache License, Version 2.0. See the NOTICE file at the root // of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 -using Core.Domain; +using Core.Domain.Abstractions; +using Core.Domain.Exceptions; using Core.Domain.Repositories; using Core.Infrastructure.Nexus; using Microsoft.Extensions.Logging; using Quartz; -using System.Net.Http.Json; namespace Core.Infrastructure.Jobs { @@ -19,12 +19,14 @@ public class ProcessEmailsJob : IJob private readonly ICustomerRepository _customerRepository; private readonly ITransactionRepository _transactionRepository; private readonly IUnitOfWork _unitOfWork; + private readonly ISendGridMailService _sendGridMailService; public ProcessEmailsJob(HttpClient client, ILogger logger, IMailsRepository mailsRepository, ICustomerRepository customerRepository, ITransactionRepository transactionRepository, + ISendGridMailService sendGridMailService, IUnitOfWork unitOfWork) { _client = client; @@ -32,6 +34,7 @@ public ProcessEmailsJob(HttpClient client, _mailsRepository = mailsRepository; _customerRepository = customerRepository; _transactionRepository = transactionRepository; + _sendGridMailService = sendGridMailService; _unitOfWork = unitOfWork; } @@ -41,82 +44,47 @@ public async Task Execute(IJobExecutionContext context) if (mails.Any()) { - // Integrate sendgrid to send emails foreach (var mail in mails) { - // call customer repo for customer name - string customerName = string.Empty; - var customerCode = mail.References.CustomerCode; + var customerCode = mail.References?.CustomerCode; - var customer = await _customerRepository.GetAsync(customerCode, context.CancellationToken); + if (string.IsNullOrWhiteSpace(customerCode)) + { + throw new CustomErrorsException("MailService", "customerCode", "An error occured while sending mail."); + } - + var customer = await _customerRepository.GetAsync(customerCode, context.CancellationToken); - if (customer != null) + if (mail.References == null || string.IsNullOrWhiteSpace(mail.References.TransactionCode)) { - customerName = customer.GetName(); + throw new CustomErrorsException("MailService", "transactionCode", "An error occured while sending mail."); } - // call transaction repo for tx details var transaction = await _transactionRepository.GetByCodeAsync(mail.References.TransactionCode, context.CancellationToken); + decimal amount = 0; + if (transaction != null) + { + amount = transaction.Amount; + } + try + { + await _sendGridMailService.SendMailAsync(mail, customer, amount); + } + catch (Exception ex) + { + _logger.LogError("An error occured sending email {code} with message {message}", mail.Code, ex.Message); + } - // call mail service also pass customerName, tx amount - - - //// Check the MailType before sending email - //// For Payout, the MailType should be TransactionSellFinish - //if (mail.Type == MailType.TransactionSellFinish.ToString()) - //{ - // // This is for Payout, so use payout template on sendgrid - //} + // once email has been sent, call nexus to update the status of this mail to 'Sent' + await _mailsRepository.UpdateMailSent(mail.Code); } } - - - - //if (callbacks.Any()) - //{ - // _logger.LogInformation("Sending callbacks"); - - // foreach (var callback in callbacks) - // { - // var body = new - // { - // callback.Code, - // callback.Content, - // CreatedOn = DateTimeProvider.ToUnixTimeInMilliseconds(callback.CreatedOn), - // Type = callback.Type.ToString() - // }; - - // try - // { - // var response = await _client.PostAsJsonAsync(callback.DestinationUrl, body); - - // if (response.IsSuccessStatusCode) - // { - // callback.Sent(); - // } - // else - // { - // callback.Failed(); - // } - // } - // catch (Exception ex) - // { - // _logger.LogError("An error occured sending callback {code} with message {message}", callback.Code, ex.Message); - // callback.Failed(); - // } - - - // _callbackRepository.Update(callback); - // } - - await _unitOfWork.SaveChangesAsync(); - } + await _unitOfWork.SaveChangesAsync(); } } +} diff --git a/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusMailsRepository.cs b/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusMailsRepository.cs index be487bcb..7d4e47f2 100644 --- a/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusMailsRepository.cs +++ b/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusMailsRepository.cs @@ -42,6 +42,15 @@ public async Task> GetMailsAsync(string status, CancellationTo return items; } + public async Task UpdateMailSent(string code, CancellationToken cancellationToken = default) + { + Task response = await _tokenServer.Compliance.Mails.UpdateMailSent(code); + + var mail = response.Result; + + return ConvertToMailAsync(mail, cancellationToken); + } + private static Mail ConvertToMailAsync(MailsResponse mail, CancellationToken cancellationToken = default) { var response = new Mail @@ -51,23 +60,23 @@ private static Mail ConvertToMailAsync(MailsResponse mail, CancellationToken can Status = mail.Status, Content = new Domain.Entities.MailAggregate.MailContent { - Html = mail.Content.Html, - Subject = mail.Content.Subject, - Text = mail.Content.Text + Html = mail.Content?.Html, + Subject = mail.Content?.Subject, + Text = mail.Content?.Text }, Count = mail.Count, Created = mail.Created, Recipient = new Domain.Entities.MailAggregate.MailRecipient { - BCC = mail.Recipient.BCC, - CC = mail.Recipient.CC, - Email = mail.Recipient.Email + BCC = mail.Recipient?.BCC, + CC = mail.Recipient?.CC, + Email = mail.Recipient!.Email }, References = new Domain.Entities.MailAggregate.MailEntityCodes { - AccountCode = mail.References.AccountCode, - CustomerCode = mail.References.CustomerCode, - TransactionCode = mail.References.TransactionCode + AccountCode = mail.References?.AccountCode, + CustomerCode = mail.References?.CustomerCode, + TransactionCode = mail.References?.TransactionCode }, Sent = mail.Sent }; diff --git a/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusTransactionRepository.cs b/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusTransactionRepository.cs index 5aebb6c8..195d07a6 100644 --- a/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusTransactionRepository.cs +++ b/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusTransactionRepository.cs @@ -7,6 +7,7 @@ using Core.Domain.Primitives; using Core.Domain.Repositories; using Core.Infrastructure.Nexus.SigningService; +using Microsoft.Extensions.Logging; using Nexus.Sdk.Token; using Nexus.Sdk.Token.Responses; @@ -109,22 +110,36 @@ public async Task> GetAsync(string publicKey, int page, int p }; } - public async Task GetByCodeAsync(string transactionCode, CancellationToken cancellationToken = default) + public async Task GetByCodeAsync(string transactionCode, CancellationToken cancellationToken = default) { var response = await _tokenServer.Operations.Get(transactionCode); - - return new Transaction() + if (response != null) { - TransactionCode = response.Code, - Amount = response.Amount, - Created = DateTimeOffset.Parse(response.Created), - Status = response.Status, - TokenCode = response.TokenCode, - Type = response.Type, - ToAccountCode = response.ReceiverAccount?.AccountCode, - FromAccountCode = response.SenderAccount?.AccountCode, - Memo = response?.Memo - }; + Transaction? transaction = new() + { + TransactionCode = response.Code, + Amount = response.Amount, + Created = DateTimeOffset.Parse(response.Created), + Status = response.Status, + TokenCode = response.TokenCode, + Type = response.Type, + ToAccountCode = response.ReceiverAccount?.AccountCode, + FromAccountCode = response.SenderAccount?.AccountCode, + Memo = response?.Memo, + + Payment = response?.Type == "Payment" + ? await _paymentRepository.HasTransactionAsync(response.Code, cancellationToken) + ? await _paymentRepository.GetByTransactionCodeAsync(response.Code, cancellationToken) + : null + : null + }; + + return transaction; + } + else + { + return null; + } } #region private methods From 2382ca0ce68a76028bbcb120e1a0a5c3adbd8a19 Mon Sep 17 00:00:00 2001 From: "neha@quantoz" Date: Thu, 23 Nov 2023 15:13:36 +0100 Subject: [PATCH 4/7] Updated the mail response --- backend/core/src/Core.API/Core.API.csproj | 4 ++-- .../Core.API/DependencyInjection/InfrastructureInjection.cs | 3 +++ .../core/src/Core.Infrastructure/Core.Infrastructure.csproj | 2 +- .../Nexus/Repositories/NexusMailsRepository.cs | 6 ++---- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/backend/core/src/Core.API/Core.API.csproj b/backend/core/src/Core.API/Core.API.csproj index f4c55484..315d28f9 100644 --- a/backend/core/src/Core.API/Core.API.csproj +++ b/backend/core/src/Core.API/Core.API.csproj @@ -23,8 +23,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/backend/core/src/Core.API/DependencyInjection/InfrastructureInjection.cs b/backend/core/src/Core.API/DependencyInjection/InfrastructureInjection.cs index 382732f2..b3caf828 100644 --- a/backend/core/src/Core.API/DependencyInjection/InfrastructureInjection.cs +++ b/backend/core/src/Core.API/DependencyInjection/InfrastructureInjection.cs @@ -177,6 +177,9 @@ public static IServiceCollection AddBackgroundJobs(this IServiceCollection servi // Register the ProcessPaymentRequestsJob, loading the schedule from configuration q.AddJobAndTrigger(configuration); + + // Register the ProcessEmailsJob, loading the schedule from configuration + q.AddJobAndTrigger(configuration); }); services.AddQuartzHostedService(opt => diff --git a/backend/core/src/Core.Infrastructure/Core.Infrastructure.csproj b/backend/core/src/Core.Infrastructure/Core.Infrastructure.csproj index ffaf9578..c6fc9172 100644 --- a/backend/core/src/Core.Infrastructure/Core.Infrastructure.csproj +++ b/backend/core/src/Core.Infrastructure/Core.Infrastructure.csproj @@ -9,7 +9,7 @@ - + diff --git a/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusMailsRepository.cs b/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusMailsRepository.cs index 7d4e47f2..a296626e 100644 --- a/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusMailsRepository.cs +++ b/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusMailsRepository.cs @@ -44,11 +44,9 @@ public async Task> GetMailsAsync(string status, CancellationTo public async Task UpdateMailSent(string code, CancellationToken cancellationToken = default) { - Task response = await _tokenServer.Compliance.Mails.UpdateMailSent(code); + var response = await _tokenServer.Compliance.Mails.UpdateMailSent(code); - var mail = response.Result; - - return ConvertToMailAsync(mail, cancellationToken); + return ConvertToMailAsync(response, cancellationToken); } private static Mail ConvertToMailAsync(MailsResponse mail, CancellationToken cancellationToken = default) From e44c64a0b422b6b215d6394c26daaf116ef28279 Mon Sep 17 00:00:00 2001 From: "neha@quantoz" Date: Mon, 11 Dec 2023 15:05:54 +0100 Subject: [PATCH 5/7] SendGrid email integration --- backend/core/src/Core.API/Core.API.csproj | 4 +- .../InfrastructureInjection.cs | 16 +++ backend/core/src/Core.API/appsettings.json | 1 + .../Abstractions/ISendGridMailService.cs | 3 +- .../core/src/Core.Domain/DateTimeProvider.cs | 5 + .../Entities/MailAggregate/Mail.cs | 2 +- .../TransactionAggregate/Transaction.cs | 2 + .../Repositories/ITransactionRepository.cs | 2 +- .../SendGridMailService/MailTemplate.cs | 35 ++++++ .../SendGridMailService.cs | 47 ++++---- .../SendGridMailServiceOptions.cs | 2 +- .../Core.Infrastructure.csproj | 2 +- .../Jobs/ProcessEmailsJob.cs | 26 +++-- .../src/Core.Infrastructure/Nexus/Enums.cs | 3 +- .../Repositories/NexusMailsRepository.cs | 2 +- .../NexusTransactionRepository.cs | 101 +++++++++++++----- .../Models/Responses/TransactionResponse.cs | 3 + 17 files changed, 192 insertions(+), 64 deletions(-) create mode 100644 backend/core/src/Core.Infrastructure/Compliance/SendGridMailService/MailTemplate.cs diff --git a/backend/core/src/Core.API/Core.API.csproj b/backend/core/src/Core.API/Core.API.csproj index 315d28f9..0e3ffc16 100644 --- a/backend/core/src/Core.API/Core.API.csproj +++ b/backend/core/src/Core.API/Core.API.csproj @@ -23,8 +23,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/backend/core/src/Core.API/DependencyInjection/InfrastructureInjection.cs b/backend/core/src/Core.API/DependencyInjection/InfrastructureInjection.cs index b3caf828..636cb3f6 100644 --- a/backend/core/src/Core.API/DependencyInjection/InfrastructureInjection.cs +++ b/backend/core/src/Core.API/DependencyInjection/InfrastructureInjection.cs @@ -7,6 +7,7 @@ using Core.Infrastructure.Compliance; using Core.Infrastructure.Compliance.IPLocator; using Core.Infrastructure.Compliance.Sanctionlist; +using Core.Infrastructure.Compliance.SendGridMailService; using Core.Infrastructure.CustomerFileStorage; using Core.Infrastructure.Jobs; using Core.Infrastructure.Nexus; @@ -31,6 +32,7 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi .AddBlobStorage(configuration) .AddCompliance(configuration) .AddTOTPGenerator() + .AddSendGridMailService(configuration) .AddBackgroundJobs(configuration); return services; @@ -166,6 +168,20 @@ private static IServiceCollection AddTOTPGenerator(this IServiceCollection servi return services; } + private static IServiceCollection AddSendGridMailService(this IServiceCollection services, IConfiguration configuration) + { + services.AddOptions() + .Bind(configuration.GetSection("SendGridMailServiceOptions")) + .ValidateDataAnnotationsRecursively() + .ValidateOnStart(); + + services.AddSingleton(sp => sp.GetRequiredService>().Value); + + services.AddSingleton(); + + return services; + } + public static IServiceCollection AddBackgroundJobs(this IServiceCollection services, IConfiguration configuration) { services.AddQuartz(q => diff --git a/backend/core/src/Core.API/appsettings.json b/backend/core/src/Core.API/appsettings.json index 48136497..e7e587a4 100644 --- a/backend/core/src/Core.API/appsettings.json +++ b/backend/core/src/Core.API/appsettings.json @@ -69,6 +69,7 @@ "AllowedOrigins": [] }, "Quartz": { + "ProcessEmailsJob": "0 * * * * ?", "ProcessCallbacksJob": "0/5 * * * * ?", "ProcessExpiredPaymentRequestJob": "0/5 * * * * ?" }, diff --git a/backend/core/src/Core.Domain/Abstractions/ISendGridMailService.cs b/backend/core/src/Core.Domain/Abstractions/ISendGridMailService.cs index 79ee3a50..13a4cfdc 100644 --- a/backend/core/src/Core.Domain/Abstractions/ISendGridMailService.cs +++ b/backend/core/src/Core.Domain/Abstractions/ISendGridMailService.cs @@ -4,11 +4,12 @@ using Core.Domain.Entities.CustomerAggregate; using Core.Domain.Entities.MailAggregate; +using Core.Domain.Entities.TransactionAggregate; namespace Core.Domain.Abstractions { public interface ISendGridMailService { - public Task SendMailAsync(Mail mail, Customer customer, decimal amount); + public Task SendMailAsync(Mail mail, Customer customer, Transaction transaction); } } \ No newline at end of file diff --git a/backend/core/src/Core.Domain/DateTimeProvider.cs b/backend/core/src/Core.Domain/DateTimeProvider.cs index b57368ba..365635d1 100644 --- a/backend/core/src/Core.Domain/DateTimeProvider.cs +++ b/backend/core/src/Core.Domain/DateTimeProvider.cs @@ -34,6 +34,11 @@ public static long ToUnixTimeInMilliseconds(DateTimeOffset dateTime) { return dateTime.ToUnixTimeMilliseconds(); } + + public static string? FormatDateTimeWithoutMilliseconds(DateTimeOffset? dateTime) + { + return dateTime?.ToString("yyyy-MM-dd HH:mm:ss"); + } } public class DateTimeProviderContext : IDisposable diff --git a/backend/core/src/Core.Domain/Entities/MailAggregate/Mail.cs b/backend/core/src/Core.Domain/Entities/MailAggregate/Mail.cs index 8bcc8993..b43a1423 100644 --- a/backend/core/src/Core.Domain/Entities/MailAggregate/Mail.cs +++ b/backend/core/src/Core.Domain/Entities/MailAggregate/Mail.cs @@ -31,7 +31,7 @@ public class MailEntityCodes public string? CustomerCode { get; set; } - public string? TransactionCode { get; set; } + public string? TokenPaymentCode { get; set; } } public class MailContent diff --git a/backend/core/src/Core.Domain/Entities/TransactionAggregate/Transaction.cs b/backend/core/src/Core.Domain/Entities/TransactionAggregate/Transaction.cs index 2d958eb8..1e69b1e1 100644 --- a/backend/core/src/Core.Domain/Entities/TransactionAggregate/Transaction.cs +++ b/backend/core/src/Core.Domain/Entities/TransactionAggregate/Transaction.cs @@ -18,6 +18,8 @@ public class Transaction public required DateTimeOffset Created { get; set; } + public DateTimeOffset? Finished { get; set; } + public required string Status { get; set; } public required string Type { get; set; } diff --git a/backend/core/src/Core.Domain/Repositories/ITransactionRepository.cs b/backend/core/src/Core.Domain/Repositories/ITransactionRepository.cs index 38717255..f84e6776 100644 --- a/backend/core/src/Core.Domain/Repositories/ITransactionRepository.cs +++ b/backend/core/src/Core.Domain/Repositories/ITransactionRepository.cs @@ -15,7 +15,7 @@ public interface ITransactionRepository public Task> GetAsync(string publicKey, int page, int pageSize, CancellationToken cancellationToken = default); - public Task GetByCodeAsync(string code, CancellationToken cancellationToken = default); + public Task> GetAsync(Dictionary? additionalParams = null, CancellationToken cancellationToken = default); public Task GetWithdrawFeesAsync(Withdraw withdraw, CancellationToken cancellationToken = default); } diff --git a/backend/core/src/Core.Infrastructure/Compliance/SendGridMailService/MailTemplate.cs b/backend/core/src/Core.Infrastructure/Compliance/SendGridMailService/MailTemplate.cs new file mode 100644 index 00000000..3b831014 --- /dev/null +++ b/backend/core/src/Core.Infrastructure/Compliance/SendGridMailService/MailTemplate.cs @@ -0,0 +1,35 @@ +// Copyright 2023 Quantoz Technology B.V. and contributors. Licensed +// under the Apache License, Version 2.0. See the NOTICE file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +using Newtonsoft.Json; + +namespace Core.Infrastructure.Compliance.SendGridMailService +{ + public class MailTemplate + { + [JsonProperty("customerFullName")] + public string? CustomerFullName { get; set; } + + [JsonProperty("amount")] + public string? Amount { get; set; } + + [JsonProperty("accountCode")] + public string? AccountCode { get; set; } + + [JsonProperty("customerBankAccount")] + public string? BankAccount { get; set; } + + [JsonProperty("transactionCode")] + public string? TransactionCode { get; set; } + + [JsonProperty("payoutAmount")] + public string? PayoutAmount { get; set; } + + [JsonProperty("createdDate")] + public string? CreatedDate { get; set; } + + [JsonProperty("finishedDate")] + public string? FinishedDate { get; set; } + } +} diff --git a/backend/core/src/Core.Infrastructure/Compliance/SendGridMailService/SendGridMailService.cs b/backend/core/src/Core.Infrastructure/Compliance/SendGridMailService/SendGridMailService.cs index dc9388d6..525bc463 100644 --- a/backend/core/src/Core.Infrastructure/Compliance/SendGridMailService/SendGridMailService.cs +++ b/backend/core/src/Core.Infrastructure/Compliance/SendGridMailService/SendGridMailService.cs @@ -2,16 +2,18 @@ // under the Apache License, Version 2.0. See the NOTICE file at the root // of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 +using Core.Domain; using Core.Domain.Abstractions; using Core.Domain.Entities.CustomerAggregate; using Core.Domain.Entities.MailAggregate; +using Core.Domain.Entities.TransactionAggregate; using Core.Domain.Exceptions; using Core.Infrastructure.Nexus; using SendGrid; using SendGrid.Helpers.Mail; using System.Net; -namespace Core.Infrastructure.Compliance.IPLocator +namespace Core.Infrastructure.Compliance.SendGridMailService { public class SendGridMailService : ISendGridMailService { @@ -25,7 +27,7 @@ public SendGridMailService( _sendGridClient = new SendGridClient(_mailOptions.ApiKey); } - public async Task SendMailAsync(Mail mail, Customer customer, decimal amount) + public async Task SendMailAsync(Mail mail, Customer customer, Transaction transaction) { if (mail == null) { @@ -33,35 +35,42 @@ public async Task SendMailAsync(Mail mail, Customer customer, decimal amount) } var from = new EmailAddress(_mailOptions.Sender); - var to = new EmailAddress(mail.Recipient?.Email); - var subject = mail.Content?.Subject; - var htmlContent = mail.Content?.Html; - var plainTextContent = mail.Content?.Text; + var to = new EmailAddress(mail.Recipient?.Email) ?? throw new CustomErrorsException("MailService", "toAddress", "An error occured while sending mail."); - if (to == null) - { - throw new CustomErrorsException("MailService", "toAddress", "An error occured while sending mail."); - } + var msg = new SendGridMessage(); - var msg = MailHelper.CreateSingleEmail(from, to, subject, plainTextContent, htmlContent); + msg.SetFrom(new EmailAddress(from.Email, from.Name)); + msg.AddTo(new EmailAddress(to.Email, to.Name)); - // If mail types is TransactionSellFinish which is Payout + // Payout if (mail.Type == MailType.TransactionSellFinish.ToString()) { msg.SetTemplateId(_mailOptions.Templates.WithdrawalTemplateID); } - else // convert to else if + else if (mail.Type == MailType.TransactionBuyFinish.ToString()) { msg.SetTemplateId(_mailOptions.Templates.FundingtemplateID); } // Fill in the dynamic template fields - msg.AddSubstitution("{{ customerFullName }}", customer?.GetName()); - msg.AddSubstitution("{{ amount }}", amount.ToString()); - msg.AddSubstitution("{{ accountCode }}", mail.References?.AccountCode); - msg.AddSubstitution("{{ customerBankAccount }}", customer?.BankAccount); - msg.AddSubstitution("{{ transactionCode }}", mail.References?.TransactionCode); - msg.AddSubstitution("{{ createdDate }}", mail.Created); + var templateData = new MailTemplate() + { + CustomerFullName = customer?.GetName(), + AccountCode = mail.References?.AccountCode, + TransactionCode = mail.References?.TokenPaymentCode, + BankAccount = customer?.BankAccount, + Amount = transaction.Amount.ToString(), + CreatedDate = DateTimeProvider.FormatDateTimeWithoutMilliseconds(transaction.Created), + FinishedDate = DateTimeProvider.FormatDateTimeWithoutMilliseconds(transaction.Finished) + }; + + if (mail.Type == MailType.TransactionSellFinish.ToString()) + { + //TODO: set payout amount when transaction details in nexus api would also return the net fiat amount + //templateData.PayoutAmount = transaction.NetFiatAmount.ToString() + } + + msg.SetTemplateData(templateData); var response = await _sendGridClient.SendEmailAsync(msg); diff --git a/backend/core/src/Core.Infrastructure/Compliance/SendGridMailService/SendGridMailServiceOptions.cs b/backend/core/src/Core.Infrastructure/Compliance/SendGridMailService/SendGridMailServiceOptions.cs index 8f5b9911..6e245732 100644 --- a/backend/core/src/Core.Infrastructure/Compliance/SendGridMailService/SendGridMailServiceOptions.cs +++ b/backend/core/src/Core.Infrastructure/Compliance/SendGridMailService/SendGridMailServiceOptions.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations; -namespace Core.Infrastructure.Compliance.IPLocator +namespace Core.Infrastructure.Compliance.SendGridMailService { public class SendGridMailServiceOptions { diff --git a/backend/core/src/Core.Infrastructure/Core.Infrastructure.csproj b/backend/core/src/Core.Infrastructure/Core.Infrastructure.csproj index c6fc9172..34a9e9f9 100644 --- a/backend/core/src/Core.Infrastructure/Core.Infrastructure.csproj +++ b/backend/core/src/Core.Infrastructure/Core.Infrastructure.csproj @@ -9,7 +9,7 @@ - + diff --git a/backend/core/src/Core.Infrastructure/Jobs/ProcessEmailsJob.cs b/backend/core/src/Core.Infrastructure/Jobs/ProcessEmailsJob.cs index 33432f22..008f8179 100644 --- a/backend/core/src/Core.Infrastructure/Jobs/ProcessEmailsJob.cs +++ b/backend/core/src/Core.Infrastructure/Jobs/ProcessEmailsJob.cs @@ -3,17 +3,18 @@ // of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 using Core.Domain.Abstractions; +using Core.Domain.Entities.TransactionAggregate; using Core.Domain.Exceptions; using Core.Domain.Repositories; using Core.Infrastructure.Nexus; using Microsoft.Extensions.Logging; +using Org.BouncyCastle.Asn1.Ocsp; using Quartz; namespace Core.Infrastructure.Jobs { public class ProcessEmailsJob : IJob { - private readonly HttpClient _client; private readonly ILogger _logger; private readonly IMailsRepository _mailsRepository; private readonly ICustomerRepository _customerRepository; @@ -21,7 +22,7 @@ public class ProcessEmailsJob : IJob private readonly IUnitOfWork _unitOfWork; private readonly ISendGridMailService _sendGridMailService; - public ProcessEmailsJob(HttpClient client, + public ProcessEmailsJob( ILogger logger, IMailsRepository mailsRepository, ICustomerRepository customerRepository, @@ -29,7 +30,6 @@ public ProcessEmailsJob(HttpClient client, ISendGridMailService sendGridMailService, IUnitOfWork unitOfWork) { - _client = client; _logger = logger; _mailsRepository = mailsRepository; _customerRepository = customerRepository; @@ -42,7 +42,7 @@ public async Task Execute(IJobExecutionContext context) { var mails = _mailsRepository.GetMailsAsync(MailStatus.ReadyToSend.ToString(), context.CancellationToken).Result; - if (mails.Any()) + if (mails != null && mails.Any()) { foreach (var mail in mails) { @@ -55,23 +55,27 @@ public async Task Execute(IJobExecutionContext context) var customer = await _customerRepository.GetAsync(customerCode, context.CancellationToken); - if (mail.References == null || string.IsNullOrWhiteSpace(mail.References.TransactionCode)) + if (mail.References == null || string.IsNullOrWhiteSpace(mail.References.TokenPaymentCode)) { - throw new CustomErrorsException("MailService", "transactionCode", "An error occured while sending mail."); + throw new CustomErrorsException("MailService", "TokenPaymentCode", "An error occured while sending mail."); } - var transaction = await _transactionRepository.GetByCodeAsync(mail.References.TransactionCode, context.CancellationToken); + var queryParams = new Dictionary + { + { "code", mail.References.TokenPaymentCode } + }; - decimal amount = 0; + var transactions = await _transactionRepository.GetAsync(queryParams, context.CancellationToken); - if (transaction != null) + Transaction? transaction = null; + if (transactions != null && transactions.Items.Any()) { - amount = transaction.Amount; + transaction = transactions.Items.FirstOrDefault(); } try { - await _sendGridMailService.SendMailAsync(mail, customer, amount); + await _sendGridMailService.SendMailAsync(mail, customer, transaction!); } catch (Exception ex) { diff --git a/backend/core/src/Core.Infrastructure/Nexus/Enums.cs b/backend/core/src/Core.Infrastructure/Nexus/Enums.cs index efedb9e1..0787ebf8 100644 --- a/backend/core/src/Core.Infrastructure/Nexus/Enums.cs +++ b/backend/core/src/Core.Infrastructure/Nexus/Enums.cs @@ -34,6 +34,7 @@ public enum MailStatus public enum MailType { - TransactionSellFinish = 1 + TransactionSellFinish = 1, + TransactionBuyFinish = 2 } } diff --git a/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusMailsRepository.cs b/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusMailsRepository.cs index a296626e..f81ccea5 100644 --- a/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusMailsRepository.cs +++ b/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusMailsRepository.cs @@ -74,7 +74,7 @@ private static Mail ConvertToMailAsync(MailsResponse mail, CancellationToken can { AccountCode = mail.References?.AccountCode, CustomerCode = mail.References?.CustomerCode, - TransactionCode = mail.References?.TransactionCode + TokenPaymentCode = mail.References?.TokenPaymentCode }, Sent = mail.Sent }; diff --git a/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusTransactionRepository.cs b/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusTransactionRepository.cs index 195d07a6..d7b576c7 100644 --- a/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusTransactionRepository.cs +++ b/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusTransactionRepository.cs @@ -7,7 +7,6 @@ using Core.Domain.Primitives; using Core.Domain.Repositories; using Core.Infrastructure.Nexus.SigningService; -using Microsoft.Extensions.Logging; using Nexus.Sdk.Token; using Nexus.Sdk.Token.Responses; @@ -110,36 +109,58 @@ public async Task> GetAsync(string publicKey, int page, int p }; } - public async Task GetByCodeAsync(string transactionCode, CancellationToken cancellationToken = default) + public async Task> GetAsync(Dictionary? additionalParams = null, CancellationToken cancellationToken = default) { - var response = await _tokenServer.Operations.Get(transactionCode); - if (response != null) + int page = 1; // default value + int pageSize = 10; // default value + + var query = new Dictionary(); + + if (additionalParams != null) { - Transaction? transaction = new() + // Check if the dictionary contains "page" key and parse its value + if (additionalParams.TryGetValue("page", out var pageValue) && int.TryParse(pageValue, out var parsedPage)) + { + page = parsedPage; + } + + // Check if the dictionary contains "limit" key and parse its value + if (additionalParams.TryGetValue("limit", out var pageSizeValue) && int.TryParse(pageSizeValue, out var parsedPageSize)) + { + pageSize = parsedPageSize; + } + + query.Add("page", page.ToString()); + query.Add("limit", pageSize.ToString()); + + foreach (var kvp in additionalParams) { - TransactionCode = response.Code, - Amount = response.Amount, - Created = DateTimeOffset.Parse(response.Created), - Status = response.Status, - TokenCode = response.TokenCode, - Type = response.Type, - ToAccountCode = response.ReceiverAccount?.AccountCode, - FromAccountCode = response.SenderAccount?.AccountCode, - Memo = response?.Memo, - - Payment = response?.Type == "Payment" - ? await _paymentRepository.HasTransactionAsync(response.Code, cancellationToken) - ? await _paymentRepository.GetByTransactionCodeAsync(response.Code, cancellationToken) - : null - : null - }; - - return transaction; + if (kvp.Value != null) + { + query.Add(kvp.Key, kvp.Value); + } + } } - else + + var response = await _tokenServer.Operations.Get(query); + + var operations = response.Records; + + var items = new List(); + + foreach (var operation in operations) { - return null; + var item = await ConvertAsync(operation, cancellationToken); + items.Add(item); } + + return new Paged + { + Items = items, + Page = page, + PageSize = pageSize, + Total = response.Total + }; } #region private methods @@ -282,6 +303,36 @@ private async Task ConvertToTransactionAsync(string publicKey, Toke return transaction; } + + private async Task ConvertAsync(TokenOperationResponse operation, CancellationToken cancellationToken = default) + { + var transaction = new Transaction + { + Amount = operation.Amount, + FromAccountCode = operation.SenderAccount?.AccountCode, + ToAccountCode = operation.ReceiverAccount?.AccountCode, + Created = DateTimeOffset.Parse(operation.Created), + Finished = operation.Finished != null ? DateTimeOffset.Parse(operation.Finished) : null, + Status = operation.Status, + TokenCode = operation.TokenCode, + TransactionCode = operation.Code, + Memo = operation.Memo, + Type = operation.Type + }; + + if (transaction.Type == "Payment") + { + var hasTransaction = await _paymentRepository.HasTransactionAsync(transaction.TransactionCode, cancellationToken); + + if (hasTransaction) + { + transaction.Payment = await _paymentRepository.GetByTransactionCodeAsync(transaction.TransactionCode, cancellationToken); + } + } + + return transaction; + } + #endregion } } diff --git a/backend/core/src/Core.Presentation/Models/Responses/TransactionResponse.cs b/backend/core/src/Core.Presentation/Models/Responses/TransactionResponse.cs index cea3cadd..de8cd813 100644 --- a/backend/core/src/Core.Presentation/Models/Responses/TransactionResponse.cs +++ b/backend/core/src/Core.Presentation/Models/Responses/TransactionResponse.cs @@ -25,6 +25,8 @@ public class TransactionResponse public long Created { get; set; } + public long? Finished { get; set; } + public required string Status { get; set; } public required string Type { get; set; } @@ -41,6 +43,7 @@ public static TransactionResponse FromTransaction(Transaction transaction) ToAccountCode = transaction.ToAccountCode, Amount = transaction.Amount, Created = DateTimeProvider.ToUnixTimeInMilliseconds(transaction.Created), + Finished = DateTimeProvider.ToUnixTimeInMilliseconds(transaction.Finished), Direction = transaction.Direction, Status = transaction.Status, TokenCode = transaction.TokenCode, From e07964a638133c1d554121ce383e31901afcee00 Mon Sep 17 00:00:00 2001 From: "neha@quantoz" Date: Mon, 11 Dec 2023 17:16:27 +0100 Subject: [PATCH 6/7] Updated token sdk url and code --- backend/core/src/Core.API/Core.API.csproj | 4 +-- .../Repositories/ITransactionRepository.cs | 2 +- .../Core.Infrastructure.csproj | 2 +- .../Jobs/ProcessEmailsJob.cs | 8 +---- .../NexusTransactionRepository.cs | 32 ++----------------- 5 files changed, 7 insertions(+), 41 deletions(-) diff --git a/backend/core/src/Core.API/Core.API.csproj b/backend/core/src/Core.API/Core.API.csproj index 0e3ffc16..54635bdf 100644 --- a/backend/core/src/Core.API/Core.API.csproj +++ b/backend/core/src/Core.API/Core.API.csproj @@ -23,8 +23,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/backend/core/src/Core.Domain/Repositories/ITransactionRepository.cs b/backend/core/src/Core.Domain/Repositories/ITransactionRepository.cs index f84e6776..63a4f9b3 100644 --- a/backend/core/src/Core.Domain/Repositories/ITransactionRepository.cs +++ b/backend/core/src/Core.Domain/Repositories/ITransactionRepository.cs @@ -15,7 +15,7 @@ public interface ITransactionRepository public Task> GetAsync(string publicKey, int page, int pageSize, CancellationToken cancellationToken = default); - public Task> GetAsync(Dictionary? additionalParams = null, CancellationToken cancellationToken = default); + public Task> GetByCodeAsync(string code, CancellationToken cancellationToken = default); public Task GetWithdrawFeesAsync(Withdraw withdraw, CancellationToken cancellationToken = default); } diff --git a/backend/core/src/Core.Infrastructure/Core.Infrastructure.csproj b/backend/core/src/Core.Infrastructure/Core.Infrastructure.csproj index 34a9e9f9..844c30f4 100644 --- a/backend/core/src/Core.Infrastructure/Core.Infrastructure.csproj +++ b/backend/core/src/Core.Infrastructure/Core.Infrastructure.csproj @@ -9,7 +9,7 @@ - + diff --git a/backend/core/src/Core.Infrastructure/Jobs/ProcessEmailsJob.cs b/backend/core/src/Core.Infrastructure/Jobs/ProcessEmailsJob.cs index 008f8179..a906d2ae 100644 --- a/backend/core/src/Core.Infrastructure/Jobs/ProcessEmailsJob.cs +++ b/backend/core/src/Core.Infrastructure/Jobs/ProcessEmailsJob.cs @@ -8,7 +8,6 @@ using Core.Domain.Repositories; using Core.Infrastructure.Nexus; using Microsoft.Extensions.Logging; -using Org.BouncyCastle.Asn1.Ocsp; using Quartz; namespace Core.Infrastructure.Jobs @@ -60,12 +59,7 @@ public async Task Execute(IJobExecutionContext context) throw new CustomErrorsException("MailService", "TokenPaymentCode", "An error occured while sending mail."); } - var queryParams = new Dictionary - { - { "code", mail.References.TokenPaymentCode } - }; - - var transactions = await _transactionRepository.GetAsync(queryParams, context.CancellationToken); + var transactions = await _transactionRepository.GetByCodeAsync(mail.References.TokenPaymentCode, context.CancellationToken); Transaction? transaction = null; if (transactions != null && transactions.Items.Any()) diff --git a/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusTransactionRepository.cs b/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusTransactionRepository.cs index d7b576c7..0f3aa7b7 100644 --- a/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusTransactionRepository.cs +++ b/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusTransactionRepository.cs @@ -109,40 +109,12 @@ public async Task> GetAsync(string publicKey, int page, int p }; } - public async Task> GetAsync(Dictionary? additionalParams = null, CancellationToken cancellationToken = default) + public async Task> GetByCodeAsync(string code, CancellationToken cancellationToken = default) { int page = 1; // default value int pageSize = 10; // default value - var query = new Dictionary(); - - if (additionalParams != null) - { - // Check if the dictionary contains "page" key and parse its value - if (additionalParams.TryGetValue("page", out var pageValue) && int.TryParse(pageValue, out var parsedPage)) - { - page = parsedPage; - } - - // Check if the dictionary contains "limit" key and parse its value - if (additionalParams.TryGetValue("limit", out var pageSizeValue) && int.TryParse(pageSizeValue, out var parsedPageSize)) - { - pageSize = parsedPageSize; - } - - query.Add("page", page.ToString()); - query.Add("limit", pageSize.ToString()); - - foreach (var kvp in additionalParams) - { - if (kvp.Value != null) - { - query.Add(kvp.Key, kvp.Value); - } - } - } - - var response = await _tokenServer.Operations.Get(query); + var response = await _tokenServer.Operations.Get(code); var operations = response.Records; From 3145d519a9468470c7ce854f205a90a8d3507352 Mon Sep 17 00:00:00 2001 From: nagarwal4 <107046041+nagarwal4@users.noreply.github.com> Date: Tue, 12 Dec 2023 14:25:19 +0100 Subject: [PATCH 7/7] Update appsettings.json --- backend/core/src/Core.API/appsettings.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backend/core/src/Core.API/appsettings.json b/backend/core/src/Core.API/appsettings.json index e7e587a4..dfb294eb 100644 --- a/backend/core/src/Core.API/appsettings.json +++ b/backend/core/src/Core.API/appsettings.json @@ -59,6 +59,14 @@ "BlobStorageOptions": { "StorageConnectionString": "", "ContainerName": "" + }, + "SendGridMailServiceOptions": { + "ApiKey": "", + "Sender": "", + "Templates": { + "WithdrawalTemplateID": "", + "FundingtemplateID": "" + } }, "ConnectionStrings": { "Database": ""