using System.Security.Cryptography; using System.Text; using System.Text.Json; using Dapr.Client; using GB5Shared.DaprCache; using GB5Shared.DTO.Framework.Login; using GB5Shared.QueryExecutor; using GB5Shared.Resource.Response; using IDMSDAL.DTOs; using IDMSDAL.Interfaces; using IDMSBLL.Interfaces; using static GB5Shared.GB5Constant.Constant; using GB5Shared.EntityHandler; namespace IDMSBLL.Implementations; public class SignOffBLL : ISignOffBLL { private readonly ISignOffDAL _SignOffDAL; private readonly IDomainItemDAL _DomainItemDAL; private readonly IQueryExecutor _QueryExecutor; private readonly KeyInvalidate _KeyInvalidate; private readonly BaseEntityAppService _BaseEntityAppService; private readonly DaprClient _DaprClient; public SignOffBLL( ISignOffDAL signOffDAL, IDomainItemDAL domainItemDAL, IQueryExecutor queryExecutor, KeyInvalidate keyInvalidate, BaseEntityAppService baseEntityAppService, DaprClient daprClient) { _SignOffDAL = signOffDAL; _DomainItemDAL = domainItemDAL; _QueryExecutor = queryExecutor; _KeyInvalidate = keyInvalidate; _BaseEntityAppService = baseEntityAppService; _DaprClient = daprClient; } // ── Reads ───────────────────────────────────────────────────────────────── public async Task GetSignOffAsync( int signOffId, LoginDTO login, CancellationToken ct) { return await _SignOffDAL.GetSignOffAsync(signOffId, login, ct) .ConfigureAwait(false); } public async Task> GetSignOffsByEngagementAsync( int engagementId, LoginDTO login, CancellationToken ct) { return await _SignOffDAL.GetSignOffsByEngagementAsync(engagementId, login, ct) .ConfigureAwait(false); } // ── Sign-off ────────────────────────────────────────────────────────────── public async Task SignOffDomainAsync( int engagementId, string domainCode, string signOffType, string signOffName, int signedById, string signedByName, string? ipAddress, LoginDTO login, CancellationToken ct) { if (string.IsNullOrWhiteSpace(domainCode)) throw new ArgumentException("DomainCode is required."); if (string.IsNullOrWhiteSpace(signOffType)) throw new ArgumentException("SignOffType is required."); if (string.IsNullOrWhiteSpace(signedByName)) throw new ArgumentException("SignedByName is required."); var items = (await _DomainItemDAL.GetDeliveryItemsByDomainAsync(engagementId, domainCode, login, ct) .ConfigureAwait(false)).ToList(); if (items.Count == 0) throw new InvalidOperationException($"No delivery items found for domain {domainCode}."); var incomplete = items.Where(i => i.Status != 3).ToList(); if (incomplete.Count > 0) throw new InvalidOperationException( $"Cannot sign off domain {domainCode}: {incomplete.Count} item(s) are not yet completed."); // Build content snapshot for the immutable SHA-256 hash var contentSnapshot = items.Select(i => new { i.DeliveryItemId, i.ItemName, i.DomainCode, i.Status, i.CompletedOn, i.CompletedById, i.ActualHours }).ToList(); var snapshotJson = JsonSerializer.Serialize(contentSnapshot); var contentHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(snapshotJson))); var dto = new SignOffDTO { SignOffId = 0, // set by IDENTITY on insert EngagementId = engagementId, DomainCode = domainCode, SignOffType = signOffType, SignOffName = signOffName, SignedById = signedById, SignedByName = signedByName, IpAddress = ipAddress, ContentSnapshot = snapshotJson, ContentHash = contentHash, SignedItems = JsonSerializer.Serialize(items.Select(i => i.DeliveryItemId)), TenantId = login.ClientId, }; var Trans = await _QueryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { await _BaseEntityAppService.ExecuteSaveAsync( EntityConstant.OBJECTIDMSSIGNOFF, EventTypeConstant.SAVEIDMSSIGNOFFEVENTTYPEID, dto, login, async tx => { int newId = await _SignOffDAL.InsertSignOffAsync(dto, login, tx, ct).ConfigureAwait(false); dto.SignOffId = newId; foreach (var item in items) { await _DomainItemDAL.MarkItemImmutableAsync( item.DeliveryItemId, contentHash, login, tx, ct).ConfigureAwait(false); } return dto.SignOffId; }, new Dictionary { { "EngagementId", engagementId } }, BIZTRANSACTIONCLASSCONSTANT.IDMSSIGNOFF, BizTransactionConstant.IDMSSIGNOFF, Trans).ConfigureAwait(false); await _QueryExecutor.CommitAsync(Trans).ConfigureAwait(false); await _DaprClient.PublishEventAsync( "pubsub", "idms.domain.signoff.completed", new { EngagementId = engagementId, DomainCode = domainCode, ContentHash = contentHash, TenantId = login.ClientId }, ct).ConfigureAwait(false); var cacheKey = new CacheKeyGeneration().KeyGeneration( engagementId, EntityConstant.OBJECTIDMSSIGNOFF, CacheKeyLevel.CLIENT_LEVEL, login); await _KeyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); return SuccessResponse.SaveSuccess; } catch (Exception) { await _QueryExecutor.RollbackAsync(Trans).ConfigureAwait(false); throw; } } }