From d23347c299fe700120b09a8d89f47c6a3a2f1a1e Mon Sep 17 00:00:00 2001 From: rowell_m_soriano Date: Mon, 2 Mar 2026 12:25:08 +0800 Subject: [PATCH] PR item addition in existing PRNo --- CPRNIMS.Domain/Contracts/PR/IPRequest.cs | 5 +- CPRNIMS.Domain/Services/Items/Item.cs | 3 +- CPRNIMS.Domain/Services/PR/PRequest.cs | 221 ++++++++---------- CPRNIMS.Domain/UIContracts/PR/IPRequest.cs | 1 + CPRNIMS.Domain/UIServices/PR/PRequest.cs | 7 +- .../Dto/SMTP/EmailValidationResult.cs | 21 ++ .../Helper/EmailValidationService.cs | 176 ++++++++++++++ CPRNIMS.Infrastructure/Helper/SMTPHelper.cs | 103 +++++--- .../ViewModel/PR/PRItemListRequest.cs | 14 ++ CPRNIMS.Infrastructure/ViewModel/PR/PRVM.cs | 1 + CPRNIMS.WebApi/Common/ServiceExtensions.cs | 1 + .../Controllers/PO/POMgmtController.cs | 2 +- .../Controllers/PR/PRMgmtController.cs | 29 +++ .../Controllers/PR/PRMgmtController.cs | 33 ++- CPRNIMS.WebApps/Views/ItemMgmt/Index.cshtml | 2 +- CPRNIMS.WebApps/Views/PRMgmt/Index.cshtml | 42 +++- .../Shared/PagesView/PO/_POScripts.cshtml | 2 +- .../Shared/PagesView/PR/_PRScripts.cshtml | 6 +- .../Shared/PagesView/PR/_PRTracking.cshtml | 31 +++ 19 files changed, 540 insertions(+), 160 deletions(-) create mode 100644 CPRNIMS.Infrastructure/Dto/SMTP/EmailValidationResult.cs create mode 100644 CPRNIMS.Infrastructure/Helper/EmailValidationService.cs create mode 100644 CPRNIMS.Infrastructure/ViewModel/PR/PRItemListRequest.cs diff --git a/CPRNIMS.Domain/Contracts/PR/IPRequest.cs b/CPRNIMS.Domain/Contracts/PR/IPRequest.cs index c6eeb01..542f112 100644 --- a/CPRNIMS.Domain/Contracts/PR/IPRequest.cs +++ b/CPRNIMS.Domain/Contracts/PR/IPRequest.cs @@ -31,6 +31,8 @@ namespace CPRNIMS.Domain.Contracts.PR Task> GetApproverName(PRDto PRDto); Task> GetApproverNameByPRNo(PRDto PRDto); Task> GetProjectCodes(PRDto pRDto); + Task> GetRemovedPR(PRDto pRDto); + Task> GetApprovedPR(PRDto pRDto); Task PRItemRemoval(PRDto pRDto); Task PostPRApproveReject(PRDto PRDto); Task PostPutReceiving(PRDto PRDto); @@ -39,7 +41,6 @@ namespace CPRNIMS.Domain.Contracts.PR Task PostPutDeniedItem(PRDto PRDto); Task PutSupplierAlterOffer(PRDto pRDto); Task PostPutProjectCode(PRDto prDto); - Task> GetRemovedPR(PRDto pRDto); - Task> GetApprovedPR(PRDto pRDto); + Task PostItemInPR(PRDto dto); } } diff --git a/CPRNIMS.Domain/Services/Items/Item.cs b/CPRNIMS.Domain/Services/Items/Item.cs index e0e0416..dfb27b7 100644 --- a/CPRNIMS.Domain/Services/Items/Item.cs +++ b/CPRNIMS.Domain/Services/Items/Item.cs @@ -135,7 +135,8 @@ namespace CPRNIMS.Domain.Services.Items public async Task> GetItemList(ItemCodeDto itemCode) { var allItems = await _dbContext.ItemList - .FromSqlRaw($"EXEC GetItemList @UserId = '{itemCode.UserId}'") + .FromSqlRaw($"EXEC GetItemList @UserId", + new SqlParameter("@UserId",itemCode.UserId)) .ToListAsync(); return allItems ?? new List(); diff --git a/CPRNIMS.Domain/Services/PR/PRequest.cs b/CPRNIMS.Domain/Services/PR/PRequest.cs index 81c7afa..9cc6ef8 100644 --- a/CPRNIMS.Domain/Services/PR/PRequest.cs +++ b/CPRNIMS.Domain/Services/PR/PRequest.cs @@ -4,16 +4,16 @@ using CPRNIMS.Infrastructure.Dto.PR; using CPRNIMS.Infrastructure.Entities.Common; using CPRNIMS.Infrastructure.Entities.Purchasing; using CPRNIMS.Infrastructure.Entities.SMTP; +using CPRNIMS.Infrastructure.Models.Common; using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; -using static CPRNIMS.Domain.Services.OutputParamMessage; +using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -using Newtonsoft.Json.Linq; -using CPRNIMS.Infrastructure.Models.Common; +using static CPRNIMS.Domain.Services.OutputParamMessage; namespace CPRNIMS.Domain.Services.PR { @@ -25,6 +25,37 @@ namespace CPRNIMS.Domain.Services.PR _dbContext = dbContext; } #region Get + public async Task> GetNotificationById(PRDto PRDto) + { + var allItems = await _dbContext.NotificationByIds + .FromSqlRaw("EXEC GetNotificationById @UserId,@PRDetailsId,@AppsModuleId", + new SqlParameter("@UserId", PRDto.UserId), + new SqlParameter("@PRDetailsId", PRDto.PRDetailsId), + new SqlParameter("@AppsModuleId", PRDto.AppsModuleId)) + .ToListAsync(); + + return allItems ?? new List(); + } + public async Task> GetProjectCodes(PRDto pRDto) + { + return await _dbContext.ProjectCodes.ToListAsync(); + } + public async Task> GetRemovedPR(PRDto pRDto) + { + var allItems = await _dbContext.RemovedPRs + .FromSqlRaw("EXEC GetRemovedPR @UserId", + new SqlParameter("@UserId", pRDto.UserId)).ToListAsync(); + + return allItems ?? new List(); + } + public async Task> GetApprovedPR(PRDto pRDto) + { + var allItems = await _dbContext.ApprovedPrs + .FromSqlRaw("EXEC GetApprovedPR @UserId", + new SqlParameter("@UserId", pRDto.UserId)).ToListAsync(); + + return allItems ?? new List(); + } public async Task> GetAllPR(PRDto PRDto) { var allItems = await _dbContext.PRLists @@ -202,90 +233,58 @@ namespace CPRNIMS.Domain.Services.PR #region Post Put public async Task PostPRApproveReject(PRDto PRDto) { - try - { - await _dbContext.Database - .ExecuteSqlRawAsync("EXEC PostPRApproveReject @UserId, @ItemNo, @Status, @PRDetailsId, @Remarks", - new SqlParameter("@ItemNo", PRDto.ItemNo != null ? PRDto.ItemNo : 0L), - new SqlParameter("@UserId", PRDto.UserId), - new SqlParameter("@Status", PRDto.Status), - new SqlParameter("@PRDetailsId", PRDto.PRDetailsId), - new SqlParameter("@Remarks", PRDto.Remarks ?? "N/A")); - return new PRDetails(); - } - catch (SqlException ex) - { - ex.ToString(); - throw; - } + await _dbContext.Database + .ExecuteSqlRawAsync("EXEC PostPRApproveReject @UserId, @ItemNo, @Status, @PRDetailsId, @Remarks", + new SqlParameter("@ItemNo", PRDto.ItemNo != null ? PRDto.ItemNo : 0L), + new SqlParameter("@UserId", PRDto.UserId), + new SqlParameter("@Status", PRDto.Status), + new SqlParameter("@PRDetailsId", PRDto.PRDetailsId), + new SqlParameter("@Remarks", PRDto.Remarks ?? "N/A")); + return new PRDetails(); } public async Task PutItemDetail(PRDto PRDto) { - try - { - await _dbContext.Database - .ExecuteSqlRawAsync($"EXEC PutPRItemDetail @UserId, @ItemLocalId, @UOMId, @ItemColorId," + - $"@Qty,@ItemCategoryId,@PRDetailsId,@Remarks,@ItemName,@ItemDescription", - new SqlParameter("@PRDetailsId", PRDto.PRDetailsId != null ? PRDto.PRDetailsId : 0L), - new SqlParameter("@ItemLocalId", PRDto.ItemLocalId), - new SqlParameter("@UOMId", PRDto.UOMId), - new SqlParameter("@ItemColorId", PRDto.ItemColorId), - new SqlParameter("@UserId", PRDto.UserId), - new SqlParameter("@Qty", PRDto.Qty), - new SqlParameter("@ItemCategoryId", PRDto.ItemCategoryId), - new SqlParameter("@Remarks", PRDto.Remarks), - new SqlParameter("@ItemName", PRDto.ItemName), - new SqlParameter("@ItemDescription", PRDto.ItemDescription)); - return new PRDetails(); - } - catch (SqlException ex) - { - ex.ToString(); - throw; - } + await _dbContext.Database + .ExecuteSqlRawAsync($"EXEC PutPRItemDetail @UserId, @ItemLocalId, @UOMId, @ItemColorId," + + $"@Qty,@ItemCategoryId,@PRDetailsId,@Remarks,@ItemName,@ItemDescription", + new SqlParameter("@PRDetailsId", PRDto.PRDetailsId != null ? PRDto.PRDetailsId : 0L), + new SqlParameter("@ItemLocalId", PRDto.ItemLocalId), + new SqlParameter("@UOMId", PRDto.UOMId), + new SqlParameter("@ItemColorId", PRDto.ItemColorId), + new SqlParameter("@UserId", PRDto.UserId), + new SqlParameter("@Qty", PRDto.Qty), + new SqlParameter("@ItemCategoryId", PRDto.ItemCategoryId), + new SqlParameter("@Remarks", PRDto.Remarks), + new SqlParameter("@ItemName", PRDto.ItemName), + new SqlParameter("@ItemDescription", PRDto.ItemDescription)); + return new PRDetails(); } public async Task PostPutDeniedItem(PRDto PRDto) { - try - { - await _dbContext.Database + await _dbContext.Database .ExecuteSqlRawAsync("EXEC PostPutDeniedItem @UserId,@PRDetailsId,@Remarks", - new SqlParameter("@UserId", PRDto.UserId), + new SqlParameter("@UserId", PRDto.UserId), new SqlParameter("@PRDetailsId", PRDto.PRDetailsId), new SqlParameter("@Remarks", PRDto.Remarks ?? "N/A")); - return new PRDetails(); - } - catch (SqlException ex) - { - ex.ToString(); - throw; - } + return new PRDetails(); } public async Task PostPutReceiving(PRDto PRDto) { - try - { - await _dbContext.Database - .ExecuteSqlRawAsync($"EXEC PostPutReceiving @UserId, @PONo, @POTypeId, @EmailAddress, @DRNo, @DocTypeId, @QuantityReceived,@RRNo,@PRDetailsId,@Remarks,@ReceivedDate,@IsCompleted", - new SqlParameter("@UserId", PRDto.UserId), - new SqlParameter("@PONo", PRDto.PONo), - new SqlParameter("@POTypeId", PRDto.POTypeId), - new SqlParameter("@EmailAddress", PRDto.EmailAddress), - new SqlParameter("@DRNo", PRDto.DRNo), - new SqlParameter("@DocTypeId", PRDto.DocTypeId), - new SqlParameter("@QuantityReceived", PRDto.QuantityReceived), - new SqlParameter("@RRNo", PRDto.RRNo), - new SqlParameter("@PRDetailsId", PRDto.PRDetailsId), - new SqlParameter("@Remarks", PRDto.Remarks ?? "N/A"), - new SqlParameter("@ReceivedDate", PRDto.ReceivedDate), - new SqlParameter("@IsCompleted", PRDto.IsCompleted)); - return new PRDetails(); - } - catch (SqlException ex) - { - ex.ToString(); - throw; - } + await _dbContext.Database + .ExecuteSqlRawAsync($"EXEC PostPutReceiving @UserId, @PONo, @POTypeId, @EmailAddress, @DRNo, @DocTypeId, @QuantityReceived,@RRNo,@PRDetailsId,@Remarks,@ReceivedDate,@IsCompleted", + new SqlParameter("@UserId", PRDto.UserId), + new SqlParameter("@PONo", PRDto.PONo), + new SqlParameter("@POTypeId", PRDto.POTypeId), + new SqlParameter("@EmailAddress", PRDto.EmailAddress), + new SqlParameter("@DRNo", PRDto.DRNo), + new SqlParameter("@DocTypeId", PRDto.DocTypeId), + new SqlParameter("@QuantityReceived", PRDto.QuantityReceived), + new SqlParameter("@RRNo", PRDto.RRNo), + new SqlParameter("@PRDetailsId", PRDto.PRDetailsId), + new SqlParameter("@Remarks", PRDto.Remarks ?? "N/A"), + new SqlParameter("@ReceivedDate", PRDto.ReceivedDate), + new SqlParameter("@IsCompleted", PRDto.IsCompleted)); + return new PRDetails(); } public async Task PutPOClose(PRDto PRDto) { @@ -310,21 +309,14 @@ namespace CPRNIMS.Domain.Services.PR new SqlParameter("@CanvassDetailId", pRDto.CanvassDetailId)); return new AlternativeOfferDetails(); } - - public async Task> GetNotificationById(PRDto PRDto) + private async Task IsUsingAsync(int projectCodeId) { - var allItems = await _dbContext.NotificationByIds - .FromSqlRaw("EXEC GetNotificationById @UserId,@PRDetailsId,@AppsModuleId", - new SqlParameter("@UserId", PRDto.UserId), - new SqlParameter("@PRDetailsId", PRDto.PRDetailsId), - new SqlParameter("@AppsModuleId", PRDto.AppsModuleId)) - .ToListAsync(); - - return allItems ?? new List(); - } - public async Task> GetProjectCodes(PRDto pRDto) - { - return await _dbContext.ProjectCodes.ToListAsync(); + return await (from pr in _dbContext.PRs + join pod in _dbContext.PODetails on pr.PRNo equals pod.PRNo + where pr.ProjectCodeId == projectCodeId + && !pod.IsRemoved + && pr.IsActive + select pr).AnyAsync(); } public async Task PRItemRemoval(PRDto prDto) { @@ -396,40 +388,27 @@ namespace CPRNIMS.Domain.Services.PR success = true }; } - private async Task IsUsingAsync(int projectCodeId) + + public async Task PostItemInPR(PRDto dto) { - try + var (messCode, message) = CreateOutputParams(); + + await _dbContext.Database.ExecuteSqlRawAsync( + "EXEC PostItemInPR @UserId,@ItemNo,@Qty,@PRNo,@MessCode OUTPUT,@Message OUTPUT", + new SqlParameter("@UserId", dto.UserId), + new SqlParameter("@ItemNo", dto.ItemNo), + new SqlParameter("@Qty", dto.Qty), + new SqlParameter("@PRNo", dto.PRNo), + messCode, + message + ); + + var response = new ResponseObject { - return await (from pr in _dbContext.PRs - join pod in _dbContext.PODetails on pr.PRNo equals pod.PRNo - where pr.ProjectCodeId == projectCodeId - && !pod.IsRemoved - && pr.IsActive - select pr).AnyAsync(); - } - catch (Exception ex) - { - ex.ToString(); - throw; - } - } - - public async Task> GetRemovedPR(PRDto pRDto) - { - var allItems = await _dbContext.RemovedPRs - .FromSqlRaw("EXEC GetRemovedPR @UserId", - new SqlParameter("@UserId", pRDto.UserId)).ToListAsync(); - - return allItems ?? new List(); - } - - public async Task> GetApprovedPR(PRDto pRDto) - { - var allItems = await _dbContext.ApprovedPrs - .FromSqlRaw("EXEC GetApprovedPR @UserId", - new SqlParameter("@UserId", pRDto.UserId)).ToListAsync(); - - return allItems ?? new List(); + message = message.Value?.ToString(), + messCode = Convert.ToByte(messCode.Value) + }; + return response; } #endregion diff --git a/CPRNIMS.Domain/UIContracts/PR/IPRequest.cs b/CPRNIMS.Domain/UIContracts/PR/IPRequest.cs index 4121dd3..683dd1f 100644 --- a/CPRNIMS.Domain/UIContracts/PR/IPRequest.cs +++ b/CPRNIMS.Domain/UIContracts/PR/IPRequest.cs @@ -44,6 +44,7 @@ namespace CPRNIMS.Domain.UIContracts.PR Task ApprovedSelectedPRItem(User user, PRVM viewModel); Task PostPutProjectCode(User user, PRVM viewModel); Task PostPutAttachment(User user, PRVM prVM); + Task PostItemInPR(User user, PRVM viewModel); #endregion } } diff --git a/CPRNIMS.Domain/UIServices/PR/PRequest.cs b/CPRNIMS.Domain/UIServices/PR/PRequest.cs index 80b95bb..335ddf7 100644 --- a/CPRNIMS.Domain/UIServices/PR/PRequest.cs +++ b/CPRNIMS.Domain/UIServices/PR/PRequest.cs @@ -7,7 +7,6 @@ using CPRNIMS.Infrastructure.ViewModel.PR; using Microsoft.Extensions.Configuration; using System.Text; using System.Text.Json; -using System.Threading.Tasks; namespace CPRNIMS.Domain.UIServices.PR { @@ -286,6 +285,12 @@ namespace CPRNIMS.Domain.UIServices.PR return await SendPostApiRequest(user, prVM, _configuration["LLI:NonInvent:PRMgmt:PostPutAttachment"]); } + + public async Task PostItemInPR(User user, PRVM prVM) + { + return await SendPostApiRequest(user, prVM, + _configuration["LLI:NonInvent:PRMgmt:PostItemInPR"]); + } #endregion } } \ No newline at end of file diff --git a/CPRNIMS.Infrastructure/Dto/SMTP/EmailValidationResult.cs b/CPRNIMS.Infrastructure/Dto/SMTP/EmailValidationResult.cs new file mode 100644 index 0000000..6a18927 --- /dev/null +++ b/CPRNIMS.Infrastructure/Dto/SMTP/EmailValidationResult.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CPRNIMS.Infrastructure.Dto.SMTP +{ + public class EmailValidationResult + { + public string Email { get; set; } = string.Empty; + public bool IsValid { get; set; } + public string Reason { get; set; } = string.Empty; + + public static EmailValidationResult Pass(string email) => + new() { Email = email, IsValid = true, Reason = "OK" }; + + public static EmailValidationResult Fail(string email, string reason) => + new() { Email = email, IsValid = false, Reason = reason }; + } +} diff --git a/CPRNIMS.Infrastructure/Helper/EmailValidationService.cs b/CPRNIMS.Infrastructure/Helper/EmailValidationService.cs new file mode 100644 index 0000000..854aeed --- /dev/null +++ b/CPRNIMS.Infrastructure/Helper/EmailValidationService.cs @@ -0,0 +1,176 @@ +using CPRNIMS.Infrastructure.Dto.SMTP; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace CPRNIMS.Infrastructure.Helper +{ + public class EmailValidationService + { + // In-memory cache for MX lookups { domain -> (isValid, timestamp) } + private static readonly Dictionary _mxCache = new(); + private static readonly TimeSpan _mxCacheExpiry = TimeSpan.FromHours(24); + + // Persistent bounce list - load from your DB or file + private static readonly HashSet _bounceList = new(StringComparer.OrdinalIgnoreCase); + + // Known disposable/fake email domains (expand this list as needed) + private static readonly HashSet _disposableDomains = new(StringComparer.OrdinalIgnoreCase) + { + // Disposable/temp mail providers + "mailinator.com", "guerrillamail.com", "tempmail.com", "throwaway.email", + "yopmail.com", "sharklasers.com", "guerrillamailblock.com", "grr.la", + "guerrillamail.info", "spam4.me", "trashmail.com", "trashmail.me", + "trashmail.net", "trashmail.at", "trashmail.io", "trashmail.org", + "fakeinbox.com", "mailnull.com", "spamgourmet.com", "spamgourmet.net", + "spamgourmet.org", "maildrop.cc", "dispostable.com", "mailexpire.com", + "spamex.com", "deadaddress.com", "spamfree24.org", "no-spam.ws", + "discard.email", "despam.it", "emailsensei.com", "getonemail.com", + "spamfree.eu", "spaml.com", "spammotel.com", "spamspot.com", + "tempe-mail.com", "tempinbox.com", "temp-mail.org", "emailtemporanea.net", + "crazymailing.com", "dispostable.com", "spamgob.com", "0-mail.com", + "0815.ru", "0clickemail.com", "10minutemail.com", "20minutemail.com", + "filzmail.com", "getnada.com", "incognitomail.com", "jetable.fr.nf", + "lortemail.dk", "mytempemail.com", "noclickemail.com", "nowmymail.com", + "objectmail.com", "odaymail.com", "onewaymail.com", "pookmail.com", + "privacy.net", "proxymail.eu", "rcpt.at", "rklips.com", + "shortmail.net", "sogetthis.com", "spamgob.com", "spaml.de", + "speed.1s.fr", "supergreatmail.com", "suremail.info", "tempail.com", + "tempemail.net", "temporarioemail.com.br", "thanksnospam.info", + "thisisnotmyrealemail.com", "throwam.com", "trbvm.com", "truckmail.com", + "tyldd.com", "uggsrock.com", "veryrealemail.com", "vidchart.com", + "wegwerfmail.de", "wegwerfmail.net", "wegwerfmail.org", "wh4f.org", + "whyspam.me", "willselfdestruct.com", "wronghead.com", "wuzupmail.net", + "xagloo.com", "xemaps.com", "xents.com", "xmaily.com", "xoxy.net", + "yep.it", "yogamaven.com", "yuurok.com", "zehnminutenmail.de", + "zippymail.info", "zoaxe.com", "zoemail.net", "zoemail.org" + }; + + // Known legitimate domains — skip MX lookup to save time + private static readonly HashSet _trustedDomains = new(StringComparer.OrdinalIgnoreCase) + { + "gmail.com", "yahoo.com", "outlook.com", "hotmail.com", "live.com", + "icloud.com", "me.com", "mac.com", "msn.com", "aol.com", + "protonmail.com", "proton.me", "zoho.com", "yandex.com", + "fastmail.com", "hey.com", "tutanota.com" + }; + + /// + /// Fast multi-layer email validation without SMTP handshake. + /// Average time per email: ~5-50ms (MX lookup only on unknown domains, cached after first hit). + /// + public async Task ValidateAsync(string email) + { + email = email.Trim().ToLowerInvariant(); + + // LAYER 1: Regex format check (instant) + if (!IsValidFormat(email)) + return EmailValidationResult.Fail(email, "Invalid email format"); + + var domain = email.Split('@')[1]; + + // LAYER 2: Bounce list check (instant - known bad addresses from past sends) + if (_bounceList.Contains(email)) + return EmailValidationResult.Fail(email, "Previously bounced email address"); + + // LAYER 3: Disposable/fake domain check (instant - local hashset lookup) + if (_disposableDomains.Contains(domain)) + return EmailValidationResult.Fail(email, $"Disposable email domain: {domain}"); + + // LAYER 4: Trusted domain fast-pass (instant - skip DNS for known providers) + if (_trustedDomains.Contains(domain)) + return EmailValidationResult.Pass(email); + + // LAYER 5: MX Record check with caching (fast after first lookup) + bool hasMx = await HasValidMxRecordAsync(domain); + if (!hasMx) + return EmailValidationResult.Fail(email, $"No MX record found for domain: {domain}"); + + return EmailValidationResult.Pass(email); + } + + /// + /// Validates a batch of emails concurrently for performance. + /// + public async Task> ValidateBatchAsync(IEnumerable emails) + { + var tasks = emails.Select(e => ValidateAsync(e)); + var results = await Task.WhenAll(tasks); + return results.ToList(); + } + + /// + /// Call this after a send attempt — records bounced addresses so they're + /// skipped automatically in future sends. + /// + public void RecordBounce(string email) + { + _bounceList.Add(email.Trim().ToLowerInvariant()); + Console.WriteLine($"[BounceTracker] Recorded bounced email: {email}"); + // TODO: Persist to your database here + // await _dbContext.BouncedEmails.AddAsync(new BouncedEmail { Address = email, Date = DateTime.UtcNow }); + } + + private bool IsValidFormat(string email) + { + var regex = new Regex( + @"^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$", + RegexOptions.IgnoreCase + ); + return regex.IsMatch(email); + } + + private async Task HasValidMxRecordAsync(string domain) + { + // Return cached result if still fresh + if (_mxCache.TryGetValue(domain, out var cached)) + { + if (DateTime.UtcNow - cached.CachedAt < _mxCacheExpiry) + return cached.IsValid; + } + + try + { + var processInfo = new System.Diagnostics.ProcessStartInfo("nslookup") + { + Arguments = $"-type=MX {domain}", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = System.Diagnostics.Process.Start(processInfo); + if (process == null) + { + _mxCache[domain] = (true, DateTime.UtcNow); // assume valid on error + return true; + } + + // Timeout nslookup after 5 seconds + var outputTask = process.StandardOutput.ReadToEndAsync(); + if (await Task.WhenAny(outputTask, Task.Delay(5000)) != outputTask) + { + Console.WriteLine($"[MX] DNS lookup timed out for {domain}, assuming valid."); + _mxCache[domain] = (true, DateTime.UtcNow); + return true; + } + + string output = await outputTask; + bool hasMx = Regex.IsMatch(output, @"mail exchanger", RegexOptions.IgnoreCase); + + _mxCache[domain] = (hasMx, DateTime.UtcNow); + Console.WriteLine($"[MX] {domain} → {(hasMx ? "Valid MX" : "No MX found")} (cached)"); + return hasMx; + } + catch (Exception ex) + { + Console.WriteLine($"[MX] Lookup error for {domain}: {ex.Message}"); + _mxCache[domain] = (true, DateTime.UtcNow); // assume valid on error + return true; + } + } + } +} diff --git a/CPRNIMS.Infrastructure/Helper/SMTPHelper.cs b/CPRNIMS.Infrastructure/Helper/SMTPHelper.cs index 617c64a..a2170cc 100644 --- a/CPRNIMS.Infrastructure/Helper/SMTPHelper.cs +++ b/CPRNIMS.Infrastructure/Helper/SMTPHelper.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Configuration; using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Net.Mail; using System.Text; using System.Threading.Tasks; @@ -12,8 +13,10 @@ namespace CPRNIMS.Infrastructure.Helper public class SMTPHelper { private readonly IConfiguration _configuration; - public SMTPHelper(IConfiguration configuration) + private readonly EmailValidationService _emailValidator; + public SMTPHelper(EmailValidationService emailValidator, IConfiguration configuration) { + _emailValidator = emailValidator; _configuration = configuration; } @@ -49,58 +52,104 @@ namespace CPRNIMS.Infrastructure.Helper return false; } + // 👇 Reads live from appsettings.json every time — picks up changes without restart + var excludedEmails = new HashSet( + _configuration.GetSection("Canvass:EmailSettings:ExcludedEmails") + .Get>() ?? new List(), + StringComparer.OrdinalIgnoreCase + ); + using (MailMessage myMessage = new MailMessage()) { - // Split the recipient string by semicolon and add each email address individually - var recipients = emailMessageBody.Recipient.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); - foreach (var recipient in recipients) + var recipientList = emailMessageBody.Recipient + .Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries) + .Select(r => r.Trim()) + .Where(r => !string.IsNullOrWhiteSpace(r)) + .Where(r => + { + if (excludedEmails.Contains(r)) + { + Console.WriteLine($"[Excluded] Skipping: {r}"); + return false; + } + return true; + }); + + // Validate remaining recipients concurrently + var validationResults = await _emailValidator.ValidateBatchAsync(recipientList); + + foreach (var result in validationResults) { - myMessage.To.Add(recipient.Trim()); + if (result.IsValid) + myMessage.To.Add(result.Email); + else + Console.WriteLine($"[Skipped] {result.Reason}: {result.Email}"); } - // myMessage.To.Add(emailMessageBody.Recipient); + + if (myMessage.To.Count == 0) + { + Console.WriteLine("No valid recipients after exclusion/validation. Aborting."); + return false; + } + myMessage.Sender = new MailAddress(emailMessageBody.SenderEmail); myMessage.From = new MailAddress(emailMessageBody.SenderEmail, emailMessageBody.DisplayName); - var cc = emailMessageBody.CC.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); - if(cc.Length > 0) + + // CC with exclusion too + if (!string.IsNullOrWhiteSpace(emailMessageBody.CC)) { - foreach (var ccs in cc) + var ccList = emailMessageBody.CC + .Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries) + .Select(c => c.Trim()) + .Where(c => !string.IsNullOrWhiteSpace(c)) + .Where(c => + { + if (excludedEmails.Contains(c)) + { + Console.WriteLine($"[Excluded CC] Skipping: {c}"); + return false; + } + return true; + }); + + var ccValidationResults = await _emailValidator.ValidateBatchAsync(ccList); + foreach (var result in ccValidationResults) { - myMessage.CC.Add(ccs.Trim()); + if (result.IsValid) + myMessage.CC.Add(result.Email); + else + Console.WriteLine($"[Skipped CC] {result.Reason}: {result.Email}"); } } - myMessage.Subject = emailMessageBody.Subject; - // Ensure the email body is correctly formatted HTML + myMessage.Subject = emailMessageBody.Subject; myMessage.Body = emailMessageBody.Message; myMessage.IsBodyHtml = true; - if (emailMessageBody.IsCanvass) + + if (emailMessageBody.IsCanvass && File.Exists(emailMessageBody.AttachPath)) { - if (File.Exists(emailMessageBody.AttachPath)) - { - Attachment pdfAttachment = new Attachment(emailMessageBody.AttachPath); - pdfAttachment.Name = Path.GetFileName(emailMessageBody.AttachPath); - myMessage.Attachments.Add(pdfAttachment); - } + Attachment pdfAttachment = new Attachment(emailMessageBody.AttachPath); + pdfAttachment.Name = Path.GetFileName(emailMessageBody.AttachPath); + myMessage.Attachments.Add(pdfAttachment); } + using (SmtpClient smtp = new SmtpClient(emailMessageBody.Server)) { - smtp.Port = emailMessageBody.OutGoingPort; smtp.UseDefaultCredentials = emailMessageBody.IsSuccess; - smtp.Credentials = new System.Net.NetworkCredential(emailMessageBody.UserName, emailMessageBody.NewPassword); + smtp.Credentials = new NetworkCredential(emailMessageBody.UserName, emailMessageBody.NewPassword); smtp.EnableSsl = true; try { await smtp.SendMailAsync(myMessage); - - Console.WriteLine("Email sent successfully."); + Console.WriteLine($"Email sent successfully to {myMessage.To.Count} recipient(s)."); return true; } catch (Exception ex) { - var Message = ex.InnerException?.ToString() ?? ex.Message.ToString(); - Console.WriteLine($"Error sending email: {Message}"); + var message = ex.InnerException?.ToString() ?? ex.Message; + Console.WriteLine($"Error sending email: {message}"); return false; } } @@ -109,8 +158,8 @@ namespace CPRNIMS.Infrastructure.Helper catch (Exception ex) { IsAuthError = true; - var Message = ex.InnerException?.ToString() ?? ex.Message.ToString(); - Console.WriteLine($"Error in SendEmailAsync: {Message}"); + var message = ex.InnerException?.ToString() ?? ex.Message; + Console.WriteLine($"Error in SendEmailAsync: {message}"); return false; } } diff --git a/CPRNIMS.Infrastructure/ViewModel/PR/PRItemListRequest.cs b/CPRNIMS.Infrastructure/ViewModel/PR/PRItemListRequest.cs new file mode 100644 index 0000000..151d3bb --- /dev/null +++ b/CPRNIMS.Infrastructure/ViewModel/PR/PRItemListRequest.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CPRNIMS.Infrastructure.ViewModel.PR +{ + public class PRItemListRequest + { + public List? ItemNo { get; set; } + public List? Qty { get; set; } + } +} diff --git a/CPRNIMS.Infrastructure/ViewModel/PR/PRVM.cs b/CPRNIMS.Infrastructure/ViewModel/PR/PRVM.cs index ce8f866..e9d7614 100644 --- a/CPRNIMS.Infrastructure/ViewModel/PR/PRVM.cs +++ b/CPRNIMS.Infrastructure/ViewModel/PR/PRVM.cs @@ -137,5 +137,6 @@ namespace CPRNIMS.Infrastructure.ViewModel.PR public int RemainingDays { get; set; } public ItemReceivingList? ItemList { get; set; } public PRList? PRList { get; set; } + public PRItemListRequest? PRItemListRequest { get; set; } } } diff --git a/CPRNIMS.WebApi/Common/ServiceExtensions.cs b/CPRNIMS.WebApi/Common/ServiceExtensions.cs index 4c813fe..bc83ebd 100644 --- a/CPRNIMS.WebApi/Common/ServiceExtensions.cs +++ b/CPRNIMS.WebApi/Common/ServiceExtensions.cs @@ -161,6 +161,7 @@ namespace CPRNIMS.WebApi.Common services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } } } diff --git a/CPRNIMS.WebApi/Controllers/PO/POMgmtController.cs b/CPRNIMS.WebApi/Controllers/PO/POMgmtController.cs index c3fb22a..01b329c 100644 --- a/CPRNIMS.WebApi/Controllers/PO/POMgmtController.cs +++ b/CPRNIMS.WebApi/Controllers/PO/POMgmtController.cs @@ -31,7 +31,7 @@ namespace CPRNIMS.WebApi.Controllers.PO _config = configuration; _purchaseOrder= purchaseOrder; } - #region Post Put + #region Post Put [HttpPost("PostIncShipFollowUp")] public async Task PostIncShipFollowUp([FromBody] PODto dto) { diff --git a/CPRNIMS.WebApi/Controllers/PR/PRMgmtController.cs b/CPRNIMS.WebApi/Controllers/PR/PRMgmtController.cs index 1577ca5..ff3130e 100644 --- a/CPRNIMS.WebApi/Controllers/PR/PRMgmtController.cs +++ b/CPRNIMS.WebApi/Controllers/PR/PRMgmtController.cs @@ -38,6 +38,35 @@ namespace CPRNIMS.WebApi.Controllers.PR _item = item; } #region POST PUT + [HttpPost("PostItemInPR")] + public async Task PostItemInPR([FromBody] PRVM PRDto) + { + var results = new ResponseObject(); + + if (PRDto.PRItemListRequest?.ItemNo?.Count > 0) + { + for (int i = 0; i < PRDto.PRItemListRequest.ItemNo.Count; i++) + { + var dto = new PRDto + { + ItemNo = PRDto.PRItemListRequest.ItemNo[i], + Qty = PRDto.PRItemListRequest.Qty[i], + UserId = PRDto.UserId, + PRNo = PRDto.PRNo, + }; + var result = await _pRequest.PostItemInPR(dto); + results.messCode = result.messCode; + results.message = result.message; + } + } + + return Ok(new ResponseObject() + { + success = results.messCode == 1, + messCode = results.messCode, + message = results.message ?? "Operation completed successfully" + }); + } [HttpPost("PostPutAttachment")] public async Task PostPutAttachment([FromBody] PRVM PRDto) { diff --git a/CPRNIMS.WebApps/Controllers/PR/PRMgmtController.cs b/CPRNIMS.WebApps/Controllers/PR/PRMgmtController.cs index e9c4ee2..274a33f 100644 --- a/CPRNIMS.WebApps/Controllers/PR/PRMgmtController.cs +++ b/CPRNIMS.WebApps/Controllers/PR/PRMgmtController.cs @@ -148,6 +148,7 @@ namespace CPRNIMS.WebApps.Controllers.PR return GetResponse(response); } + #region Mapper private PRList MapToPRItemList(IEnumerable prList) { if (prList == null || !prList.Any()) @@ -165,9 +166,28 @@ namespace CPRNIMS.WebApps.Controllers.PR ItemNo = prList.SelectMany(ic => ic.ItemNo).ToList() }; } + private PRItemListRequest MapToPRItemList(IEnumerable prItemListRequest) + { + if (prItemListRequest == null || !prItemListRequest.Any()) + { + return new PRItemListRequest + { + ItemNo = new List(), + Qty = new List(), + }; + } + + return new PRItemListRequest + { + ItemNo = prItemListRequest.SelectMany(ic => ic.ItemNo).ToList(), + Qty = prItemListRequest.SelectMany(ic => ic.Qty).ToList() + }; + } + #endregion #endregion #region POST PUT - public async Task UploadAttachment(IFormFile? file, [FromForm] string? oldFileName, + public async Task UploadAttachment(IFormFile? file, [FromForm] + string? oldFileName, [FromForm] long prId) { var uploadsPath = Path.Combine( @@ -326,6 +346,17 @@ namespace CPRNIMS.WebApps.Controllers.PR } return Json(new { success = false, Response = postPutItem.Message }); } + public async Task PostItemInPR(PRVM viewModel, List PRItemList) + { + viewModel.PRItemListRequest = MapToPRItemList(PRItemList); + var postPutItem = await _pRequest.PostItemInPR(GetUser(), viewModel); + + if (postPutItem.messCode != 0) + { + return Json(new { success = true, Response = postPutItem.Message }); + } + return Json(new { success = false, Response = postPutItem.Message }); + } #endregion #region Views public IActionResult GetDashBoardById(int DashboardId) diff --git a/CPRNIMS.WebApps/Views/ItemMgmt/Index.cshtml b/CPRNIMS.WebApps/Views/ItemMgmt/Index.cshtml index e30e98a..f1516d8 100644 --- a/CPRNIMS.WebApps/Views/ItemMgmt/Index.cshtml +++ b/CPRNIMS.WebApps/Views/ItemMgmt/Index.cshtml @@ -13,7 +13,7 @@ - + diff --git a/CPRNIMS.WebApps/Views/PRMgmt/Index.cshtml b/CPRNIMS.WebApps/Views/PRMgmt/Index.cshtml index 05fb02f..414d313 100644 --- a/CPRNIMS.WebApps/Views/PRMgmt/Index.cshtml +++ b/CPRNIMS.WebApps/Views/PRMgmt/Index.cshtml @@ -24,6 +24,46 @@ + +
ItemCodeNo.ItemNo ItemName ItemSpecs CategoryName
+ + + + + + + + + + + + +
AllItemNoItemNameItemSpecsCategoryNameQty
+ + + + + + + + +
@@ -499,6 +507,29 @@
+ \ No newline at end of file