using DXPDAL.Kyc; using GB5Shared.Attachment; using GB5Shared.DTO.ECM; using GB5Shared.DTO.Framework.Login; using GB5Shared.EntityHandler; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using GB5Shared.Resource.Response; using GB5Shared.Telemetry; using static GB5Shared.GB5Constant.Constant; namespace DXPBLL.Kyc; public class KycBLL : IKycBLL { private readonly IPartyKycDAL _KycDAL; private readonly IDXPKycEncryption _Encryption; private readonly IAttachmentUploadService _AttachmentUploadService; private readonly IAttachmentResolveDAL _AttachmentResolveDAL; private readonly AutoNumber _AutoNumber; private readonly IQueryExecutor _QueryExecutor; private readonly BaseEntityAppService _BaseEntityAppService; public KycBLL( IPartyKycDAL kycDAL, IDXPKycEncryption encryption, IAttachmentUploadService attachmentUploadService, IAttachmentResolveDAL attachmentResolveDAL, AutoNumber autoNumber, IQueryExecutor queryExecutor, BaseEntityAppService baseEntityAppService) { _KycDAL = kycDAL; _Encryption = encryption; _AttachmentUploadService = attachmentUploadService; _AttachmentResolveDAL = attachmentResolveDAL; _AutoNumber = autoNumber; _QueryExecutor = queryExecutor; _BaseEntityAppService = baseEntityAppService; } public async Task GetActiveByBranchLinkAsync(int dxpPartyBranchLinkId, LoginDTO login, CancellationToken ct) => await _KycDAL.GetActiveByBranchLinkAsync(dxpPartyBranchLinkId, login, ct).ConfigureAwait(false); public async Task<(string Message, int DxpPartyKycProfileId)> SubmitAsync( int dxpPartyBranchLinkId, string? gstin, string? pan, Guid rowGuid, LoginDTO login, CancellationToken ct) { if (dxpPartyBranchLinkId <= 0) throw new ArgumentException("DxpPartyBranchLinkId is required."); if (string.IsNullOrWhiteSpace(gstin) && string.IsNullOrWhiteSpace(pan)) throw new ArgumentException("At least one of GSTIN or PAN is required."); var normalizedGstin = gstin?.Trim().ToUpperInvariant(); var normalizedPan = pan?.Trim().ToUpperInvariant(); var dto = new PartyKycProfileDTO { DxpPartyBranchLinkId = dxpPartyBranchLinkId, GstinEncrypted = normalizedGstin != null ? await _Encryption.EncryptAsync(normalizedGstin, ct).ConfigureAwait(false) : null, GstinHash = normalizedGstin != null ? _Encryption.Hash(normalizedGstin) : null, PanEncrypted = normalizedPan != null ? await _Encryption.EncryptAsync(normalizedPan, ct).ConfigureAwait(false) : null, PanHash = normalizedPan != null ? _Encryption.Hash(normalizedPan) : null, VerificationStatus = 1, // Pending RowGuid = rowGuid, Status = 1 }; var Trans = await _QueryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { GB5Trace.Step("submit-dxp-kyc", new { dxpPartyBranchLinkId }); // Supersede any existing active profile for this branch link — kept for audit, // never overwritten (STATUS=2). var existing = await _KycDAL.GetActiveByBranchLinkAsync(dxpPartyBranchLinkId, login, ct).ConfigureAwait(false); if (existing != null) await _KycDAL.SupersedeAsync(existing.DxpPartyKycProfileId, login, Trans, ct).ConfigureAwait(false); var auto = await _AutoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.DXPPARTYKYCPROFILE, login) .ConfigureAwait(false); dto.DxpPartyKycProfileId = auto.StartNumber; await _BaseEntityAppService.ExecuteSaveAsync( EntityConstant.OBJECTDXPPARTYKYCPROFILE, EventTypeConstant.SAVEDXPPARTYKYCPROFILEEVENTTYPEID, dto, login, async tx => { await _KycDAL.SaveAsync(dto, login, tx, ct).ConfigureAwait(false); return dto.DxpPartyKycProfileId; }, null, -1, -1, Trans).ConfigureAwait(false); // Backfill any documents uploaded pre-save (ObjectId=0, tagged with rowGuid) to the // now-known DxpPartyKycProfileId — UAS's two-phase attachment pattern. await _AttachmentResolveDAL.ResolveAsync( EntityConstant.OBJECTDXPPARTYKYCPROFILE, rowGuid, dto.DxpPartyKycProfileId, login, ct) .ConfigureAwait(false); await _QueryExecutor.CommitAsync(Trans).ConfigureAwait(false); return ($"{SuccessResponse.SaveSuccessMessage} {dto.DxpPartyKycProfileId}", dto.DxpPartyKycProfileId); } catch (Exception ex) { await _QueryExecutor.RollbackAsync(Trans).ConfigureAwait(false); GB5Trace.MarkFailed("submit-dxp-kyc-failed", ex); throw; } } public async Task UploadDocumentAsync( AttachmentUploadRequest request, Stream fileStream, string originalFileName, string contentType, LoginDTO login, CancellationToken ct) { // Thin pass-through — UAS owns storage/versioning/deferred-resolution entirely. return await _AttachmentUploadService .UploadAsync(request, fileStream, originalFileName, contentType, login, ct) .ConfigureAwait(false); } public async Task VerifyAsync(int dxpPartyKycProfileId, bool approved, string? rejectionReason, LoginDTO login, CancellationToken ct) { var profile = await _KycDAL.GetByIdAsync(dxpPartyKycProfileId, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException("KYC profile not found."); if (!approved && string.IsNullOrWhiteSpace(rejectionReason)) throw new ArgumentException("RejectionReason is required when rejecting a KYC profile."); profile.VerificationStatus = approved ? (byte)2 : (byte)3; profile.VerifiedOn = DateTime.UtcNow; profile.VerifiedById = login.UserId; profile.RejectionReason = approved ? null : rejectionReason; profile.ModifiedById = login.UserId; profile.ModifiedOn = DateTime.UtcNow; var Trans = await _QueryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { GB5Trace.Step("verify-dxp-kyc", new { dxpPartyKycProfileId, approved }); await _BaseEntityAppService.ExecuteSaveAsync( EntityConstant.OBJECTDXPPARTYKYCPROFILE, EventTypeConstant.VERIFYDXPPARTYKYCPROFILEEVENTTYPEID, profile, login, async tx => { await _KycDAL.VerifyAsync(profile, login, tx, ct).ConfigureAwait(false); return profile.DxpPartyKycProfileId; }, null, -1, -1, Trans).ConfigureAwait(false); await _QueryExecutor.CommitAsync(Trans).ConfigureAwait(false); return SuccessResponse.UpdateSuccess; } catch (Exception ex) { await _QueryExecutor.RollbackAsync(Trans).ConfigureAwait(false); GB5Trace.MarkFailed("verify-dxp-kyc-failed", ex); throw; } } }