using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Dapr.Client; using FrameworkDAL.CustomCode.GOP; using GB5Shared.DaprCache; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.GOP; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using GB5Shared.Resource.Response; using GB5Shared.Telemetry; using GB5Shared.Validation; using System.ComponentModel.DataAnnotations; using static GB5Shared.GB5Constant.Constant; namespace FrameworkBLL.GOP { // ============================================================ // DocumentTypeBLL — a named, reusable "shape of an incoming source file/document" // (e.g. "SBI Bank Statement (Excel)"), optionally linked to a GopCanonicalModelDTO. // Same Framework/DevAdmin-protected library-tier convention as CanonicalModelBLL — // see GuardSourceType. // ============================================================ public class DocumentTypeBLL : IDocumentTypeBLL { private readonly IDocumentTypeDAL _DocumentTypeDAL; private readonly IQueryExecutor _QueryExecutor; private readonly AutoNumber _AutoNumber; private readonly IValidation _Validation; private readonly DaprClient _DaprClient; private readonly CacheKeyGeneration _KeyGen = new(); // Same bare-key, "statestore"-named Dapr convention as CanonicalModelBLL — see its // own comment for why this is NOT IDALCache (whose "dal:"-prefixed key nothing here // ever reads; GetDocumentType/GetDocumentTypeList go through BaseEndpoint's own // GetCacheKey()-driven cache, not an IDALCache.GetOrSetAsync call). private const string StateStore = "statestore"; public DocumentTypeBLL( IDocumentTypeDAL documentTypeDAL, IQueryExecutor queryExecutor, AutoNumber autoNumber, IValidation validation, DaprClient daprClient) { _DocumentTypeDAL = documentTypeDAL; _QueryExecutor = queryExecutor; _AutoNumber = autoNumber; _Validation = validation; _DaprClient = daprClient; } private async Task InvalidateCacheKey(string cacheKey, CancellationToken ct) { try { await _DaprClient.DeleteStateAsync(StateStore, cacheKey, cancellationToken: ct); } catch (Exception) { // A statestore outage here must not fail the write that already committed. } } // GetDocumentType/GetDocumentTypeList are both CacheKeyLevel.CLIENT_LEVEL. private async Task InvalidateDocumentTypeCaches(int documentTypeId, LoginDTO loginDTO, CancellationToken ct) { await InvalidateCacheKey( _KeyGen.KeyGeneration(documentTypeId, EntityConstant.GOPDOCUMENTTYPE, CacheKeyLevel.CLIENT_LEVEL, loginDTO), ct); await InvalidateCacheKey( _KeyGen.KeyGeneration(0, EntityConstant.GOPDOCUMENTTYPE, CacheKeyLevel.CLIENT_LEVEL, loginDTO), ct); } public async Task SaveDocumentType( GopDocumentTypeDTO dto, LoginDTO loginDTO, CancellationToken ct = default) { await _Validation.NotEmpty(dto.EntityCode, nameof(dto.EntityCode)); await _Validation.NotEmpty(dto.DocumentTypeName, nameof(dto.DocumentTypeName)); bool isNew = dto.DocumentTypeId == 0; GB5Trace.Step("save-document-type", new { dto.EntityCode, isNew }); var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { if (isNew) { var auto = await _AutoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.DOCUMENTTYPE, loginDTO); dto.DocumentTypeId = auto.StartNumber; await _DocumentTypeDAL.Insert(dto, loginDTO, trans, ct); } else { var existing = await _DocumentTypeDAL.GetById(dto.DocumentTypeId, loginDTO, ct); GuardSourceType(existing?.SourceType, loginDTO.SourceType); await _DocumentTypeDAL.Update(dto, loginDTO, trans, ct); } await _QueryExecutor.CommitAsync(trans); await InvalidateDocumentTypeCaches(dto.DocumentTypeId, loginDTO, ct); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {dto.DocumentTypeId}" : $"{SuccessResponse.UpdateSuccessMessage} {dto.DocumentTypeId}"; } catch (Exception ex) { await _QueryExecutor.RollbackAsync(trans); GB5Trace.MarkFailed("save-document-type-failed", ex); throw; } } public async Task GetDocumentType( int documentTypeId, LoginDTO loginDTO, CancellationToken ct = default) => await _DocumentTypeDAL.GetById(documentTypeId, loginDTO, ct); public async Task> GetDocumentTypeList( LoginDTO loginDTO, CancellationToken ct = default) => await _DocumentTypeDAL.GetList(loginDTO, ct); public async Task DeactivateDocumentType( int documentTypeId, LoginDTO loginDTO, CancellationToken ct = default) { var existing = await _DocumentTypeDAL.GetById(documentTypeId, loginDTO, ct); GuardSourceType(existing?.SourceType, loginDTO.SourceType); var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { await _DocumentTypeDAL.Deactivate(documentTypeId, loginDTO, trans, ct); await _QueryExecutor.CommitAsync(trans); await InvalidateDocumentTypeCaches(documentTypeId, loginDTO, ct); return SuccessResponse.DeleteSuccessMessage; } catch (Exception ex) { await _QueryExecutor.RollbackAsync(trans); GB5Trace.MarkFailed("deactivate-document-type-failed", ex); throw; } } // Library-tier protection — same convention as CanonicalModelBLL.GuardSourceType. private static void GuardSourceType(byte? recordSourceType, int loginSourceType) { if (recordSourceType.HasValue && recordSourceType.Value < loginSourceType) throw new ValidationException("Insufficient privilege to modify this library document type — clone it instead."); } } }