Send email after Upload document

This commit is contained in:
jojo aquino 2025-01-02 20:52:59 +00:00
parent f29e5b3c17
commit 4d5a69ad2f
17 changed files with 591 additions and 5 deletions

View File

@ -0,0 +1,45 @@
using Coravel.Invocable;
using Coravel.Mailer.Mail.Interfaces;
using EnotaryoPH.Data;
using EnotaryoPH.Web.Common.Models;
using EnotaryoPH.Web.Mailables;
namespace EnotaryoPH.Web.Common.Jobs
{
public class SignatoryInvitationInvocable : IInvocable, IInvocableWithPayload<Guid>
{
private readonly IMailer _mailer;
private readonly NotaryoDBContext _notaryoDBContext;
private readonly Settings _settings;
public SignatoryInvitationInvocable(IMailer mailer, NotaryoDBContext notaryoDBContext, Settings settings)
{
_mailer = mailer;
_notaryoDBContext = notaryoDBContext;
_settings = settings;
}
public Guid Payload { get; set; }
public async Task Invoke()
{
var signatory = _notaryoDBContext
.TransactionSignatories
.Include(e => e.Transaction)
.ThenInclude(t => t.Principal)
.FirstOrDefault(e => e.TransactionSignatory_UID == Payload);
var user = signatory.Transaction.Principal;
var name = $"{user.Firstname} {user.Lastname}".NullIfWhiteSpace() ?? user.Email;
var invitationCode = new Guid(signatory.InvitationCode).ToBase64String();
await _mailer.SendAsync(new SignatoryInvitationMailable(new SignatoryViewModel
{
Email = signatory.Email,
InvitationURL = $"{_settings.BaseUrl}/Principal/Registration/{invitationCode}",
MainPrincipalName = name,
SignatoryType = signatory.Type
}));
}
}
}

View File

@ -0,0 +1,7 @@
namespace EnotaryoPH.Web.Common.Models
{
public class Settings
{
public string BaseUrl { get; set; }
}
}

View File

@ -0,0 +1,16 @@
using Coravel.Mailer.Mail;
namespace EnotaryoPH.Web.Mailables
{
public class SignatoryInvitationMailable : Mailable<SignatoryViewModel>
{
private readonly SignatoryViewModel _model;
public SignatoryInvitationMailable(SignatoryViewModel model) => _model = model;
public override void Build() => this
.To(_model.Email)
.From("noreply@enotaryoph.com")
.View("~/Views/Mail/SignatoryInvitation.cshtml", _model);
}
}

View File

@ -0,0 +1,10 @@
namespace EnotaryoPH.Web.Mailables
{
public class SignatoryViewModel
{
public string Email { get; set; }
public string InvitationURL { get; set; }
public string MainPrincipalName { get; set; }
public string SignatoryType { get; set; }
}
}

View File

@ -0,0 +1,4 @@
@page "{InvitationCode?}"
@model EnotaryoPH.Web.Pages.Participant.Registration.IndexModel
@{
}

View File

@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace EnotaryoPH.Web.Pages.Participant.Registration
{
public class IndexModel : PageModel
{
public IActionResult OnGet() => Page();
[BindProperty(SupportsGet = true)]
public string InvitationCode { get; set; }
}
}

View File

@ -0,0 +1,94 @@
@page "{Transaction_UID}"
@using EnotaryoPH.Web.Pages.Shared.Components.NotaryoSteps
@model EnotaryoPH.Web.Pages.Principal.NotaryoSteps.ChooseNotaryModel
@{
}
@section Head {
<link href="\lib\fontawesome-free-6.7.1-web\css\all.min.css" rel="stylesheet" />
}
<section class="my-5">
<div class="container">
<div class="row">
<div class="col">
<h1>Notaryo Steps</h1>
<div class="mt-3">
@await Component.InvokeAsync("NotaryoSteps", new {
NotaryoSteps = new List<NotaryoStep>() {
new NotaryoStep { Name = "Upload Identification", Step = 1 },
new NotaryoStep { Name = "Take Selfie", Step = 2 },
new NotaryoStep { Name = "Upload Document", Step = 3 },
new NotaryoStep { Name = "Choose Notary", Step = 4, IsActive = true },
}
})
<form class="mt-3">
<div class="row my-4">
<div class="col">
<div class="d-flex">
<div class="form-check me-3"><input id="formCheck-3" class="form-check-input" type="radio" name="NewOrOld" checked /><label class="form-check-label" for="formCheck-3">I will choose a Notary Public myself</label></div>
<div class="form-check"><input id="formCheck-4" class="form-check-input" type="radio" name="NewOrOld" /><label class="form-check-label" for="formCheck-4">Automatically choose an immediately available Notary Public</label></div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12 col-lg-8">
<div class="input-group"><input class="form-control" type="text" placeholder="Find an Attorney" /><button class="btn btn-outline-secondary" type="button"><i class="fas fa-search"></i></button></div>
</div>
</div>
<div class="row mt-3">
<div class="col">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Name of Attorney</th>
<th>Office Location</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>Athony Co</td>
<td>123 Fictional Road, Manila</td>
<td class="text-end"><button class="btn btn-secondary btn-sm" type="button">Select</button></td>
</tr>
<tr>
<td>Jude Romano</td>
<td>342 Fake Road, Marikina</td>
<td class="text-end"><button class="btn btn-secondary btn-sm" type="button">Select</button></td>
</tr>
</tbody>
</table>
</div>
<nav>
<ul class="pagination">
<li class="page-item"><a class="page-link" aria-label="Previous" href="#"><span aria-hidden="true">«</span></a></li>
<li class="page-item"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item"><a class="page-link" aria-label="Next" href="#"><span aria-hidden="true">»</span></a></li>
</ul>
</nav>
</div>
</div>
<div class="d-flex">
<div class="flex-grow-1"></div>
<button class="btn btn-primary btn-lg" type="submit">
SUBMIT
<i class="fas fa-flag-checkered ms-2"></i>
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
@section Scripts {
<script src="/dist/js/jfa.min.js"></script>
}

View File

@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace EnotaryoPH.Web.Pages.Principal.NotaryoSteps
{
public class ChooseNotaryModel : PageModel
{
public void OnGet()
{
}
[BindProperty(SupportsGet = true)]
public Guid Transaction_UID { get; set; }
}
}

View File

@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
namespace EnotaryoPH.Web.Pages.Principal.NotaryoSteps
{
public class SignatoryViewModel
{
[EmailAddress, Required, BindProperty]
public string Email { get; set; }
[Required, BindProperty]
public string Type { get; set; }
public Guid UID { get; set; }
}
}

View File

@ -4,6 +4,9 @@
@{
}
@section Head {
<link href="\lib\fontawesome-free-6.7.1-web\css\all.min.css" rel="stylesheet" />
}
<section class="my-5">
<div class="container">
@ -22,7 +25,99 @@
})
<div class="tab-content mt-3"></div>
</div>
<form class="mt-5" enctype="multipart/form-data" method="post" id="UploadDocumentForm">
<div class="row mt-3">
<div class="col-12 col-lg-6">
<div class="mb-3">
<label class="form-label">Document</label>
<input class="form-control" type="file" asp-for="DocumentFile" required />
@Html.ValidationMessageFor(x => x.DocumentFile)
</div>
</div>
<div class="col">
<div class="mb-3"><label class="form-label">Document Type</label>
<select class="form-select" asp-for="DocumentType" asp-items="Model.DocumentTypes" required>
<option value="">Please choose an option</option>
</select>
</div>
@Html.ValidationMessageFor(x => x.DocumentType)
</div>
</div>
<div class="row mt-3">
<div class="col-12 col-lg-6 col-xxl-6"><label class="form-label">More than one principal?</label>
<div class="input-group">
<span class="input-group-text">Email</span>
<input class="form-control" type="email" id="NewPrincipalEmail" />
<button class="btn btn-secondary" type="button" id="AddAdditionalPrincipalButton">ADD</button>
</div>
<div class="field-validation-error">
<span id="AdditionalPrincipalsValidation"></span>
</div>
<ul class="mt-2" id="AdditionalPrincipalsList">
</ul>
</div>
<div class="col-12 col-lg-6 col-xxl-6"><label class="form-label">More than one witness?</label>
<div class="input-group">
<span class="input-group-text">Email</span>
<input class="form-control" type="email" id="NewWitnessEmail" />
<button class="btn btn-secondary" type="button" id="AddWitnessButton">ADD</button>
</div>
<div class="field-validation-error">
<span id="WitnessesValidation"></span>
</div>
<ul class="mt-2" id="WitnessesList">
</ul>
</div>
</div>
<div class="row">
<div class="col">
<fieldset><label class="form-label">Would you like to record the video conference?</label>
<div class="d-flex">
<div class="form-check me-3"><input id="formCheck-2" class="form-check-input" type="radio" name="IsRecorded" checked /><label class="form-check-label" for="formCheck-2">Yes, record the session</label></div>
<div class="form-check"><input id="formCheck-3" class="form-check-input" type="radio" name="IsRecorded" /><label class="form-check-label" for="formCheck-3">No, do not record the session</label></div>
</div>
</fieldset>
</div>
</div>
<div class="row mt-4">
<div class="col">
<div class="form-check">
<input class="form-check-input" type="checkbox" asp-for="IsConfirmed" />
<label class="form-check-label" asp-for="IsConfirmed">I confirm and attest under oath that I freely and voluntarily executed the document; that I read and understood the same; and that the contents of the document are true and correct.</label>
</div>
@Html.ValidationMessageFor(x => x.IsConfirmed)
</div>
</div>
<input type="hidden" asp-for="CurrentUserEmail" />
<input type="hidden" asp-for="ParticipantsJson" />
<div class="d-flex">
<div class="flex-grow-1"></div>
<button type="submit" class="btn btn-primary btn-lg" id="NextButton">
<span>NEXT</span>
<i class="fas fa-chevron-right ms-2"></i>
</button>
</div>
</form>
</div>
</div>
</div>
</section>
<template id="ParticipantItemTemplate">
<li class="participantitem__li">
<div class="d-flex">
<span class="participantitem__email">mail@example.com</span>
<div class="flex-grow-1"></div>
<a role="button" style="color: var(--bs-danger);" class="participantitem__delete">
<i class="fas fa-times" data-bs-toggle="tooltip" data-bss-tooltip data-bs-placement="left" title="delete signatory"></i>
</a>
</div>
</li>
</template>
@section Scripts {
<script src="/dist/js/jfa.min.js"></script>
<script src="~/Pages/Principal/NotaryoSteps/UploadDocument.cshtml.js" asp-append-version="true"></script>
}

View File

@ -1,14 +1,143 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using Coravel.Queuing.Interfaces;
using EnotaryoPH.Data;
using EnotaryoPH.Data.Entities;
using EnotaryoPH.Web.Common.Jobs;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace EnotaryoPH.Web.Pages.Principal.NotaryoSteps
{
public class UploadDocumentModel : PageModel
{
public void OnGet()
private readonly ICurrentUserService _currentUserService;
private readonly NotaryoDBContext _notaryoDBContext;
private readonly IQueue _queue;
public UploadDocumentModel(NotaryoDBContext notaryoDBContext, ICurrentUserService currentUserService, IQueue queue)
{
_notaryoDBContext = notaryoDBContext;
_currentUserService = currentUserService;
_queue = queue;
}
public IActionResult OnGet(Guid transaction_UID)
{
var _transaction = _notaryoDBContext.Transactions
.Include(t => t.TransactionDocument)
.Include(t => t.TransactionSignatories)
.AsNoTracking().FirstOrDefault(e => e.Transaction_UID == transaction_UID);
DocumentTypes = GetDocumentTypes();
CurrentUserEmail = _currentUserService.GetEmail();
var signatories = _transaction.TransactionSignatories.Select(ts => new SignatoryViewModel
{
Email = ts.Email,
Type = ts.Type,
UID = ts.TransactionSignatory_UID.GetValueOrDefault()
}).ToList();
ParticipantsJson = JsonSerializer.Serialize(signatories);
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
DocumentTypes = GetDocumentTypes();
return Page();
}
if (!IsConfirmed)
{
ModelState.AddModelError(nameof(IsConfirmed), "You must tick this box to continue.");
DocumentTypes = GetDocumentTypes();
return Page();
}
var transaction = _notaryoDBContext.Transactions
.Include(t => t.TransactionSignatories)
.Include(t => t.TransactionDocument)
.FirstOrDefault(t => t.Transaction_UID == Transaction_UID);
if (transaction == null)
{
return NotFound();
}
transaction.Status = nameof(TransactionStatus.DocumentUploaded);
transaction.IsRecorded = IsVideoConferenceRecorded;
if (transaction.TransactionDocument == null)
{
transaction.TransactionDocument = new TransactionDocument
{
CreatedOn = DateTime.UtcNow,
Transaction = transaction,
TransactionDocument_UID = Guid.CreateVersion7(DateTime.UtcNow),
};
}
var stream = new MemoryStream((int)DocumentFile.Length);
DocumentFile.CopyTo(stream);
transaction.TransactionDocument.File = stream.ToArray();
transaction.TransactionDocument.Filename = DocumentFile.FileName;
transaction.TransactionDocument.DocumentType = DocumentType;
transaction.TransactionDocument.UploadedOn = DateTime.UtcNow;
var participants = JsonSerializer.Deserialize<List<SignatoryViewModel>>(ParticipantsJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? [];
transaction.TransactionSignatories = participants.Select(p => new TransactionSignatory
{
CreatedOn = DateTime.UtcNow,
Email = p.Email,
Status = nameof(SignatoryStatus.New),
TransactionSignatory_UID = Guid.CreateVersion7(DateTime.UtcNow),
Type = p.Type,
InvitationCode = Guid.CreateVersion7(DateTime.UtcNow).ToString()
}).ToList();
_notaryoDBContext.Update(transaction);
_notaryoDBContext.SaveChanges();
foreach (var signatory in transaction.TransactionSignatories)
{
_queue.QueueInvocableWithPayload<SignatoryInvitationInvocable, Guid>(signatory.TransactionSignatory_UID.GetValueOrDefault());
}
return Redirect($"/Principal/NotaryoSteps/ChooseNotary/{Transaction_UID}");
}
private List<SelectListItem> GetDocumentTypes()
{
var lookupIdentificationTypes = _notaryoDBContext.LookupData.AsNoTracking().Include(e => e.LookupDataValues).FirstOrDefault(e => e.Name == "Document Types");
return lookupIdentificationTypes.LookupDataValues
.ConvertAll(m => new SelectListItem
{
Text = m.Title.DefaultIfEmpty(m.Value),
Value = m.Value
});
}
public string CurrentUserEmail { get; private set; }
[BindProperty, Required]
public IFormFile DocumentFile { get; set; }
[BindProperty, Required]
public string DocumentType { get; set; }
public List<SelectListItem> DocumentTypes { get; set; }
[BindProperty]
public bool IsConfirmed { get; set; }
[BindProperty]
public bool IsVideoConferenceRecorded { get; set; }
[BindProperty]
public string ParticipantsJson { get; set; } = "[]";
[BindProperty(SupportsGet = true)]
public Guid Transaction_UID { get; set; }
}

View File

@ -0,0 +1,97 @@
"use strict";
(function () {
let
control_addAdditionalPrincipalButton = document.getElementById("AddAdditionalPrincipalButton"),
control_additionalPrincipalsList = document.getElementById("AdditionalPrincipalsList"),
control_additionalPrincipalsValidation = document.getElementById("AdditionalPrincipalsValidation"),
control_addWitnessButton = document.getElementById("AddWitnessButton"),
control_currentUserEmail = document.getElementById("CurrentUserEmail"),
control_documentFile = document.getElementById("DocumentFile"),
control_isConfirmed = document.getElementById("IsConfirmed"),
control_newPrincipalEmail = document.getElementById("NewPrincipalEmail"),
control_newWitnessEmail = document.getElementById("NewWitnessEmail"),
control_participantItemTemplate = document.getElementById("ParticipantItemTemplate"),
control_participantsJson = document.getElementById("ParticipantsJson"),
control_uploadDocumentForm = document.getElementById("UploadDocumentForm"),
control_witnessesList = document.getElementById("WitnessesList"),
control_witnessesValidation = document.getElementById("WitnessesValidation");
let signatories = [];
function _addSignatory(control_email, signatoryType, control_signatoryList) {
let email = control_email.value;
if (!email) {
return "Email is required.";
}
if (control_currentUserEmail.value == email) {
return "Email must not be the same as the currently logged in user.";
}
if (signatories.find(x => x.email === email)) {
return "Email already exists.";
}
signatories.push({
email: email,
type: signatoryType
});
let liTemplate = control_participantItemTemplate.cloneNode(true);
let li = liTemplate.content;
li.querySelector(".participantitem__li").setAttribute("data-uid", "");
li.querySelector(".participantitem__email").textContent = email;
let deleteButton = li.querySelector(".participantitem__delete");
deleteButton.addEventListener("click", _deleteParticipant);
deleteButton.control_signatoryList = control_signatoryList;
control_signatoryList.appendChild(li);
control_email.value = "";
}
function _deleteParticipant(sender) {
let li = sender.target.closest("li");
let email = li.querySelector(".participantitem__email").textContent;
jfa.components.dialog.confirm({
message: "Are you sure you want to remove this participant?",
callbackyes: function () {
signatories = signatories.filter(m => m.email != email);
}
});
}
function _bindEvents() {
control_addAdditionalPrincipalButton.addEventListener("click", () => {
let err = _addSignatory(control_newPrincipalEmail, 'Principal', control_additionalPrincipalsList);
control_additionalPrincipalsValidation.textContent = err;
});
control_addWitnessButton.addEventListener("click", () => {
let err = _addSignatory(control_newWitnessEmail, 'Witness', control_witnessesList);
control_witnessesValidation.textContent = err;
});
control_uploadDocumentForm.addEventListener("submit", _onSubmitForm);
}
function _init() {
signatories = JSON.parse(control_participantsJson.value)?.map(p => ({
email: p.Email,
type: p.Type,
uid: p.UID
}));
_bindEvents();
}
function _onSubmitForm(event) {
control_participantsJson.value = JSON.stringify(signatories);
if (control_documentFile.files.length === 0 || !control_isConfirmed.checked) {
event.preventDefault();
return false;
}
return true;
}
_init();
})();

View File

@ -1,5 +1,8 @@
using System.Security.Principal;
using Coravel;
using EnotaryoPH.Data;
using EnotaryoPH.Web.Common.Jobs;
using EnotaryoPH.Web.Common.Models;
using Exadel.Compreface.Clients.CompreFaceClient;
using Microsoft.AspNetCore.Authentication.Cookies;
@ -21,20 +24,28 @@ namespace EnotaryoPH.Web
razorBuilder.AddRazorRuntimeCompilation();
#endif
var config = builder.Configuration;
builder.Services.AddDbContext<NotaryoDBContext>();
builder.Services.AddHttpContextAccessor();
builder.Services.AddSession(options => options.IdleTimeout = TimeSpan.FromMinutes(120));
builder.Services.AddTransient<IPrincipal>(provider => provider.GetService<IHttpContextAccessor>()?.HttpContext?.User);
builder.Services.AddTransient<Settings>(provider => new Settings
{
BaseUrl = config.GetValue<string>("BaseUrl") ?? ""
});
builder.Services.AddTransient<IPasswordService, PasswordService>();
builder.Services.AddTransient<ICurrentUserService, CurrentUserService>();
builder.Services.AddTransient<ICompreFaceClient>(provider =>
{
var config = provider.GetRequiredService<IConfiguration>();
var host = config.GetValue<string>("CompreFaceConfig:Host");
var port = config.GetValue<string>("CompreFaceConfig:Port");
return new CompreFaceClient(host, port);
});
builder.Services.AddQueue();
builder.Services.AddMailer(config);
builder.Services.AddTransient<SignatoryInvitationInvocable>();
var app = builder.Build();

View File

@ -0,0 +1,18 @@
@using EnotaryoPH.Web.Mailables
@model SignatoryViewModel
@{
ViewBag.Heading = "Welcome!";
ViewBag.Preview = "Example Email";
}
<p>
Hello, you have been invited to enotaryo as a @Model.SignatoryType by @Model.MainPrincipalName.
Please click here to get you started. @await Component.InvokeAsync("EmailLinkButton", new { text = "Registration Page", url = Model.InvitationURL })
</p>
@section links
{
<a href="@Model.InvitationURL">Registration Page</a>
}

View File

@ -0,0 +1,3 @@
@using EnotaryoPH.Web
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Coravel.Mailer.ViewComponents

View File

@ -0,0 +1,3 @@
@{
Layout = "~/Areas/Coravel/Pages/Mail/Template.cshtml";
}

View File

@ -13,5 +13,15 @@
"APIKey": "secret",
"Host": "http://localhost",
"Port": "8000"
}
},
"Coravel": {
"Mail": {
"Driver": "SMTP",
"Host": "localhost",
"Port": 25,
"Username": "",
"Password": ""
}
},
"BaseUrl": "https://localhost:7121"
}