using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using GB5Shared.GOP.Qualifier; using GB5Shared.GOP.Qualifier.DTOs; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.EntityHandler; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using GB5Shared.Resource.Response; using Microsoft.Extensions.Logging; using static GB5Shared.GB5Constant.Constant; namespace GB5Shared.GOP.Qualifier { // ============================================================ // QualifierBLL — Qualifier Engine Business Logic // // Save pattern (per CLAUDE.md): // BeginTransactionAsync → GetAutoNumber → ExecuteSaveAsync → CommitAsync // RollbackAsync in catch, always rethrow. // // ActivateQualifierVersion is special: it archives the current // Active version and activates the target version in a single // DB transaction — no ExecuteSaveAsync (pure UPDATE, not INSERT). // ============================================================ public class QualifierBLL : IQualifierBLL { private readonly IQualifierDAL _QualifierDAL; private readonly AutoNumber _AutoNumber; private readonly IQueryExecutor _QueryExecutor; private readonly IQualifierPlanCompiler _PlanCompiler; private readonly BaseEntityAppService _DefinitionEntitySvc; private readonly BaseEntityAppService _VersionEntitySvc; private readonly BaseEntityAppService _EntityEntitySvc; private readonly BaseEntityAppService _MethodEntitySvc; private readonly BaseEntityAppService _RuleGroupEntitySvc; private readonly BaseEntityAppService _RuleConditionEntitySvc; private readonly BaseEntityAppService _OutputEntitySvc; private readonly BaseEntityAppService _DependencyEntitySvc; private readonly BaseEntityAppService _ScopeEntitySvc; private readonly ILogger _Logger; public QualifierBLL( IQualifierDAL qualifierDAL, AutoNumber autoNumber, IQueryExecutor queryExecutor, IQualifierPlanCompiler planCompiler, BaseEntityAppService definitionEntitySvc, BaseEntityAppService versionEntitySvc, BaseEntityAppService entityEntitySvc, BaseEntityAppService methodEntitySvc, BaseEntityAppService ruleGroupEntitySvc, BaseEntityAppService ruleConditionEntitySvc, BaseEntityAppService outputEntitySvc, BaseEntityAppService dependencyEntitySvc, BaseEntityAppService scopeEntitySvc, ILogger logger) { _QualifierDAL = qualifierDAL; _AutoNumber = autoNumber; _QueryExecutor = queryExecutor; _PlanCompiler = planCompiler; _DefinitionEntitySvc = definitionEntitySvc; _VersionEntitySvc = versionEntitySvc; _EntityEntitySvc = entityEntitySvc; _MethodEntitySvc = methodEntitySvc; _RuleGroupEntitySvc = ruleGroupEntitySvc; _RuleConditionEntitySvc = ruleConditionEntitySvc; _OutputEntitySvc = outputEntitySvc; _DependencyEntitySvc = dependencyEntitySvc; _ScopeEntitySvc = scopeEntitySvc; _Logger = logger; } // ═══════════════════════════════════════════════════════ // DEFINITION // ═══════════════════════════════════════════════════════ public async Task SaveQualifierDefinition( QualifierDefinitionDTO dto, LoginDTO loginDTO, CancellationToken ct = default) { if (string.IsNullOrWhiteSpace(dto.QualifierCode)) throw new ArgumentException("QualifierCode is required."); if (string.IsNullOrWhiteSpace(dto.QualifierName)) throw new ArgumentException("QualifierName is required."); if (string.IsNullOrWhiteSpace(dto.EntityCode)) throw new ArgumentException("EntityCode is required."); var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { bool isNew = dto.QualifierId == 0; if (isNew) { var auto = await _AutoNumber.GetNumberAsync( 1, AUTONUMBERCONSTANT.QUALIFIER, loginDTO); dto.QualifierId = auto.StartNumber; } await _DefinitionEntitySvc.ExecuteSaveAsync( EntityConstant.QUALIFIER, -1, // SAVEQUALIFIEREVENTTYPEID — not yet in compiled GB5Shared.dll dto, loginDTO, async tx => { if (isNew) await _QualifierDAL.SaveQualifierDefinition(dto, loginDTO, tx, ct); else await _QualifierDAL.UpdateQualifierDefinition(dto, loginDTO, tx, ct); return dto.QualifierId; }, null, BIZTRANSACTIONCLASSCONSTANT.QUALIFIER, BizTransactionConstant.QUALIFIER, trans); await _QueryExecutor.CommitAsync(trans); _Logger.LogInformation( "Qualifier '{Code}' (id={Id}) {Action} by user {UserId}", dto.QualifierCode, dto.QualifierId, isNew ? "created" : "updated", loginDTO.UserId); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {dto.QualifierId}" : $"{SuccessResponse.UpdateSuccessMessage} {dto.QualifierId}"; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } public async Task DeleteQualifier( int qualifierId, LoginDTO loginDTO, CancellationToken ct = default) { var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { await _QualifierDAL.DeleteQualifierDefinition(qualifierId, loginDTO, trans, ct); await _QueryExecutor.CommitAsync(trans); return SuccessResponse.DeleteSuccessMessage; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } public async Task GetQualifierById( int qualifierId, LoginDTO loginDTO, CancellationToken ct = default) => await _QualifierDAL.GetQualifierById(qualifierId, loginDTO, ct); public async Task GetQualifierByCode( string qualifierCode, LoginDTO loginDTO, CancellationToken ct = default) => await _QualifierDAL.GetQualifierByCode(qualifierCode, loginDTO, ct); public async Task> GetQualifierList( LoginDTO loginDTO, CancellationToken ct = default) => await _QualifierDAL.GetQualifierList(loginDTO, ct); public async Task> GetSelectListQualifier( CriteriaDTO criteriaDTO, LoginDTO loginDTO, CancellationToken ct = default) => await _QualifierDAL.GetSelectListQualifier(criteriaDTO, loginDTO, ct); public async Task> GetQualifierLibrary( LoginDTO loginDTO, CancellationToken ct = default) => await _QualifierDAL.GetQualifierLibrary(loginDTO, ct); // ═══════════════════════════════════════════════════════ // VERSION // ═══════════════════════════════════════════════════════ public async Task SaveQualifierVersion( QualifierVersionDTO dto, LoginDTO loginDTO, CancellationToken ct = default) { if (dto.QualifierId == 0) throw new ArgumentException("QualifierId is required."); var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { var auto = await _AutoNumber.GetNumberAsync( 1, AUTONUMBERCONSTANT.QUALIFIERVERSION, loginDTO); dto.QualifierVersionId = auto.StartNumber; dto.VersionStatus = 0; // Draft — always starts as Draft await _VersionEntitySvc.ExecuteSaveAsync( EntityConstant.QUALIFIERVERSION, -1, // SAVEQUALIFIERVERSONEVENTTYPEID — not yet in compiled GB5Shared.dll dto, loginDTO, async tx => { await _QualifierDAL.SaveQualifierVersion(dto, loginDTO, tx, ct); return dto.QualifierVersionId; }, new Dictionary { { "QualifierId", dto.QualifierId } }, BIZTRANSACTIONCLASSCONSTANT.QUALIFIERVERSION, BizTransactionConstant.QUALIFIERVERSION, trans); await _QueryExecutor.CommitAsync(trans); return $"{SuccessResponse.SaveSuccessMessage} {dto.QualifierVersionId}"; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } /// /// Archives any currently Active version for the qualifier, then activates the target /// version — both operations run inside a single DB transaction (atomicity guarantee). /// The snapshotJson must be pre-compiled by the caller (QualifierPlanCompiler). /// public async Task ActivateQualifierVersion( int qualifierVersionId, int qualifierId, LoginDTO loginDTO, CancellationToken ct = default) { if (qualifierVersionId == 0) throw new ArgumentException("QualifierVersionId is required."); if (qualifierId == 0) throw new ArgumentException("QualifierId is required."); // Compile the plan before opening the transaction — compilation is read-only // (loads methods/rules/dependencies, validates SQL via GBQueryExecutor). // The compiled JSON is then stored atomically with the activation. var snapshotJson = await _PlanCompiler.CompileAsync(qualifierId, loginDTO, ct); var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { // Step 1: Archive current Active version (0 rows affected = no active version — fine). await _QualifierDAL.ArchiveActiveVersion( qualifierId, loginDTO.UserId, loginDTO, trans, ct); // Step 2: Activate the target version with the compiled snapshot. await _QualifierDAL.ActivateVersion( qualifierVersionId, snapshotJson, loginDTO.UserId, loginDTO, trans, ct); await _QueryExecutor.CommitAsync(trans); _Logger.LogInformation( "QualifierVersion {VersionId} activated for Qualifier {QualifierId} by user {UserId}", qualifierVersionId, qualifierId, loginDTO.UserId); return $"{SuccessResponse.UpdateSuccessMessage} {qualifierVersionId}"; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } public async Task GetActiveQualifierPlan( int qualifierId, LoginDTO loginDTO, CancellationToken ct = default) => await _QualifierDAL.GetActiveQualifierPlan(qualifierId, loginDTO, ct); public async Task> GetQualifierVersionList( int qualifierId, LoginDTO loginDTO, CancellationToken ct = default) => await _QualifierDAL.GetQualifierVersionList(qualifierId, loginDTO, ct); public async Task DeleteQualifierVersion( int qualifierVersionId, LoginDTO loginDTO, CancellationToken ct = default) { var status = await _QualifierDAL.GetQualifierVersionStatus(qualifierVersionId, loginDTO, ct); if (status is null) throw new ArgumentException($"QualifierVersion {qualifierVersionId} not found."); if (status != 0) throw new ArgumentException("Only a Draft version can be deleted — Active/Archived versions are audit/replay history."); var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { await _QualifierDAL.DeleteQualifierVersion(qualifierVersionId, loginDTO, trans, ct); await _QueryExecutor.CommitAsync(trans); return SuccessResponse.DeleteSuccessMessage; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } // ═══════════════════════════════════════════════════════ // ENTITY BINDING // ═══════════════════════════════════════════════════════ public async Task SaveQualifierEntity( QualifierEntityDTO dto, LoginDTO loginDTO, CancellationToken ct = default) { if (dto.QualifierId == 0) throw new ArgumentException("QualifierId is required."); if (string.IsNullOrWhiteSpace(dto.EntityCode)) throw new ArgumentException("EntityCode is required."); bool isNew = dto.QualifierEntityId == 0; var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { await _EntityEntitySvc.ExecuteSaveAsync( EntityConstant.QUALIFIERENTITY, -1, // SAVEQUALIFIERENTITYEVENTTYPEID — not yet in compiled GB5Shared.dll dto, loginDTO, async tx => { // MQUALIFIERENTITY.QUALIFIERENTITYID is a live IDENTITY column (unlike // every other GOP table) -- QualifierDAL.SaveQualifierEntity assigns // dto.QualifierEntityId from the DB-generated value, not AutoNumber. if (isNew) await _QualifierDAL.SaveQualifierEntity(dto, loginDTO, tx, ct); else await _QualifierDAL.UpdateQualifierEntity(dto, loginDTO, tx, ct); return dto.QualifierEntityId; }, new Dictionary { { "QualifierId", dto.QualifierId } }, BIZTRANSACTIONCLASSCONSTANT.QUALIFIERENTITY, BizTransactionConstant.QUALIFIERENTITY, trans, isNewEntity: isNew); await _QueryExecutor.CommitAsync(trans); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {dto.QualifierEntityId}" : SuccessResponse.UpdateSuccess; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } public async Task> GetEntityBindings( string entityCode, byte stage, byte scope, LoginDTO loginDTO, CancellationToken ct = default) => await _QualifierDAL.GetEntityBindings(entityCode, stage, scope, loginDTO, ct); public async Task> GetEntityBindingsByQualifier( int qualifierId, LoginDTO loginDTO, CancellationToken ct = default) => await _QualifierDAL.GetEntityBindingsByQualifier(qualifierId, loginDTO, ct); // ═══════════════════════════════════════════════════════ // METHOD // ═══════════════════════════════════════════════════════ public async Task SaveQualifierMethod( QualifierMethodDTO dto, LoginDTO loginDTO, CancellationToken ct = default) { if (dto.QualifierId == 0) throw new ArgumentException("QualifierId is required."); var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { await _MethodEntitySvc.ExecuteSaveAsync( EntityConstant.QUALIFIERMETHOD, -1, // SAVEQUALIFIERMETHODEVENTTYPEID — not yet in compiled GB5Shared.dll dto, loginDTO, async tx => { // MQUALIFIERMETHOD.QUALIFIERMETHODID is a live IDENTITY column (like // MQUALIFIERENTITY) -- QualifierDAL.SaveQualifierMethod assigns // dto.QualifierMethodId from the DB-generated value, not AutoNumber. await _QualifierDAL.SaveQualifierMethod(dto, loginDTO, tx, ct); return dto.QualifierMethodId; }, new Dictionary { { "QualifierId", dto.QualifierId } }, BIZTRANSACTIONCLASSCONSTANT.QUALIFIERMETHOD, BizTransactionConstant.QUALIFIERMETHOD, trans, isNewEntity: true); await _QueryExecutor.CommitAsync(trans); return $"{SuccessResponse.SaveSuccessMessage} {dto.QualifierMethodId}"; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } public async Task> GetQualifierMethods( int qualifierId, LoginDTO loginDTO, CancellationToken ct = default) => await _QualifierDAL.GetQualifierMethods(qualifierId, loginDTO, ct); public async Task DeleteQualifierMethod( int qualifierMethodId, LoginDTO loginDTO, CancellationToken ct = default) { var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { await _QualifierDAL.DeleteQualifierMethod(qualifierMethodId, loginDTO, trans, ct); await _QueryExecutor.CommitAsync(trans); return SuccessResponse.DeleteSuccessMessage; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } // ═══════════════════════════════════════════════════════ // RULE GROUP // ═══════════════════════════════════════════════════════ public async Task SaveQualifierRuleGroup( QualifierRuleGroupDTO dto, LoginDTO loginDTO, CancellationToken ct = default) { if (dto.QualifierId == 0) throw new ArgumentException("QualifierId is required."); var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { // QUALIFIERRULEGROUPID is a live IDENTITY column on GB5DEMO — QualifierDAL // assigns dto.RuleGroupId from the DB-generated value, not AutoNumber. await _RuleGroupEntitySvc.ExecuteSaveAsync( EntityConstant.QUALIFIERRULEGROUP, -1, // SAVEQUALIFIERRULEGROUPEVENTTYPEID — not yet in compiled GB5Shared.dll dto, loginDTO, async tx => { await _QualifierDAL.SaveQualifierRuleGroup(dto, loginDTO, tx, ct); return dto.RuleGroupId; }, new Dictionary { { "QualifierId", dto.QualifierId } }, BIZTRANSACTIONCLASSCONSTANT.QUALIFIERRULEGROUP, BizTransactionConstant.QUALIFIERRULEGROUP, trans, isNewEntity: true); await _QueryExecutor.CommitAsync(trans); return $"{SuccessResponse.SaveSuccessMessage} {dto.RuleGroupId}"; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } public async Task> GetRuleGroups( int qualifierId, LoginDTO loginDTO, CancellationToken ct = default) => await _QualifierDAL.GetRuleGroups(qualifierId, loginDTO, ct); public async Task DeleteQualifierRuleGroup( int ruleGroupId, LoginDTO loginDTO, CancellationToken ct = default) { var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { await _QualifierDAL.DeleteQualifierRuleGroup(ruleGroupId, loginDTO, trans, ct); await _QueryExecutor.CommitAsync(trans); return SuccessResponse.DeleteSuccessMessage; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } // ═══════════════════════════════════════════════════════ // RULE CONDITION // ═══════════════════════════════════════════════════════ public async Task SaveQualifierRuleCondition( QualifierRuleConditionDTO dto, LoginDTO loginDTO, CancellationToken ct = default) { if (dto.RuleGroupId == 0) throw new ArgumentException("RuleGroupId is required."); if (dto.QualifierId == 0) throw new ArgumentException("QualifierId is required."); if (string.IsNullOrWhiteSpace(dto.FieldPath)) throw new ArgumentException("FieldPath is required."); var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { // QUALIFIERRULECONDITIONID is a live IDENTITY column on GB5DEMO — // QualifierDAL assigns dto.ConditionId from the DB-generated value. await _RuleConditionEntitySvc.ExecuteSaveAsync( EntityConstant.QUALIFIERRULECONDITION, -1, // SAVEQUALIFIERRULECONDITIONEVENTTYPEID — not yet in compiled GB5Shared.dll dto, loginDTO, async tx => { await _QualifierDAL.SaveQualifierRuleCondition(dto, loginDTO, tx, ct); return dto.ConditionId; }, new Dictionary { { "QualifierId", dto.QualifierId }, { "RuleGroupId", dto.RuleGroupId } }, BIZTRANSACTIONCLASSCONSTANT.QUALIFIERRULECONDITION, BizTransactionConstant.QUALIFIERRULECONDITION, trans, isNewEntity: true); await _QueryExecutor.CommitAsync(trans); return $"{SuccessResponse.SaveSuccessMessage} {dto.ConditionId}"; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } public async Task> GetRuleConditions( int qualifierId, LoginDTO loginDTO, CancellationToken ct = default) => await _QualifierDAL.GetRuleConditions(qualifierId, loginDTO, ct); public async Task DeleteQualifierRuleCondition( int conditionId, LoginDTO loginDTO, CancellationToken ct = default) { var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { await _QualifierDAL.DeleteQualifierRuleCondition(conditionId, loginDTO, trans, ct); await _QueryExecutor.CommitAsync(trans); return SuccessResponse.DeleteSuccessMessage; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } // ═══════════════════════════════════════════════════════ // OUTPUT // ═══════════════════════════════════════════════════════ public async Task SaveQualifierOutput( QualifierOutputDTO dto, LoginDTO loginDTO, CancellationToken ct = default) { if (dto.QualifierId == 0) throw new ArgumentException("QualifierId is required."); if (string.IsNullOrWhiteSpace(dto.OutputKey)) throw new ArgumentException("OutputKey is required."); var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { // QUALIFIEROUTPUTID is a live IDENTITY column on GB5DEMO — QualifierDAL // assigns dto.OutputId from the DB-generated value, not AutoNumber. await _OutputEntitySvc.ExecuteSaveAsync( EntityConstant.QUALIFIEROUTPUT, -1, // SAVEQUALIFIEROUTPUTEVENTTYPEID — not yet in compiled GB5Shared.dll dto, loginDTO, async tx => { await _QualifierDAL.SaveQualifierOutput(dto, loginDTO, tx, ct); return dto.OutputId; }, new Dictionary { { "QualifierId", dto.QualifierId } }, BIZTRANSACTIONCLASSCONSTANT.QUALIFIEROUTPUT, BizTransactionConstant.QUALIFIEROUTPUT, trans, isNewEntity: true); await _QueryExecutor.CommitAsync(trans); return $"{SuccessResponse.SaveSuccessMessage} {dto.OutputId}"; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } public async Task> GetQualifierOutputs( int qualifierId, LoginDTO loginDTO, CancellationToken ct = default) => await _QualifierDAL.GetQualifierOutputs(qualifierId, loginDTO, ct); public async Task DeleteQualifierOutput( int outputId, LoginDTO loginDTO, CancellationToken ct = default) { var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { await _QualifierDAL.DeleteQualifierOutput(outputId, loginDTO, trans, ct); await _QueryExecutor.CommitAsync(trans); return SuccessResponse.DeleteSuccessMessage; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } // ═══════════════════════════════════════════════════════ // DEPENDENCY // ═══════════════════════════════════════════════════════ public async Task SaveQualifierDependency( QualifierDependencyDTO dto, LoginDTO loginDTO, CancellationToken ct = default) { if (dto.QualifierId == 0) throw new ArgumentException("QualifierId is required."); if (dto.DependsOnQualifierId == 0) throw new ArgumentException("DependsOnQualifierId is required."); if (dto.QualifierId == dto.DependsOnQualifierId) throw new ArgumentException("A qualifier cannot depend on itself."); var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { // QUALIFIERDEPENDENCYID is a live IDENTITY column on GB5DEMO — QualifierDAL // assigns dto.DependencyId from the DB-generated value, not AutoNumber. await _DependencyEntitySvc.ExecuteSaveAsync( EntityConstant.QUALIFIERDEPENDENCY, -1, // SAVEQUALIFIERDEPENDENCYEVENTTYPEID — not yet in compiled GB5Shared.dll dto, loginDTO, async tx => { await _QualifierDAL.SaveQualifierDependency(dto, loginDTO, tx, ct); return dto.DependencyId; }, new Dictionary { { "QualifierId", dto.QualifierId } }, BIZTRANSACTIONCLASSCONSTANT.QUALIFIERDEPENDENCY, BizTransactionConstant.QUALIFIERDEPENDENCY, trans, isNewEntity: true); await _QueryExecutor.CommitAsync(trans); return $"{SuccessResponse.SaveSuccessMessage} {dto.DependencyId}"; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } public async Task> GetQualifierDependencies( int qualifierId, LoginDTO loginDTO, CancellationToken ct = default) => await _QualifierDAL.GetQualifierDependencies(qualifierId, loginDTO, ct); public async Task DeleteQualifierDependency( int dependencyId, LoginDTO loginDTO, CancellationToken ct = default) { var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { await _QualifierDAL.DeleteQualifierDependency(dependencyId, loginDTO, trans, ct); await _QueryExecutor.CommitAsync(trans); return SuccessResponse.DeleteSuccessMessage; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } // ═══════════════════════════════════════════════════════ // SCOPE // ═══════════════════════════════════════════════════════ public async Task SaveQualifierScope( QualifierScopeDTO dto, LoginDTO loginDTO, CancellationToken ct = default) { if (dto.QualifierId == 0) throw new ArgumentException("QualifierId is required."); var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { // QUALIFIERSCOPEID is a live IDENTITY column on GB5DEMO — QualifierDAL // assigns dto.QualifierScopeId from the DB-generated value, not AutoNumber. await _ScopeEntitySvc.ExecuteSaveAsync( EntityConstant.QUALIFIERSCOPE, -1, // SAVEQUALIFIERSCOPEEVENTTYPEID — not yet in compiled GB5Shared.dll dto, loginDTO, async tx => { await _QualifierDAL.SaveQualifierScope(dto, loginDTO, tx, ct); return dto.QualifierScopeId; }, new Dictionary { { "QualifierId", dto.QualifierId } }, BIZTRANSACTIONCLASSCONSTANT.QUALIFIERSCOPE, BizTransactionConstant.QUALIFIERSCOPE, trans, isNewEntity: true); await _QueryExecutor.CommitAsync(trans); return $"{SuccessResponse.SaveSuccessMessage} {dto.QualifierScopeId}"; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } public async Task> GetQualifierScopes( int qualifierId, LoginDTO loginDTO, CancellationToken ct = default) => await _QualifierDAL.GetQualifierScopes(qualifierId, loginDTO, ct); public async Task DeleteQualifierScope( int qualifierScopeId, LoginDTO loginDTO, CancellationToken ct = default) { var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { await _QualifierDAL.DeleteQualifierScope(qualifierScopeId, loginDTO, trans, ct); await _QueryExecutor.CommitAsync(trans); return SuccessResponse.DeleteSuccessMessage; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } // ═══════════════════════════════════════════════════════ // CLONE // Deep-clones a qualifier definition with all its child // entities into a brand-new qualifier. Rule group // ParentRuleGroupId references are remapped to cloned IDs. // ═══════════════════════════════════════════════════════ public async Task CloneQualifier( int sourceQualifierId, string newQualifierCode, string newQualifierName, LoginDTO loginDTO, CancellationToken ct = default) { if (sourceQualifierId == 0) throw new ArgumentException("SourceQualifierId is required."); if (string.IsNullOrWhiteSpace(newQualifierCode)) throw new ArgumentException("NewQualifierCode is required."); if (string.IsNullOrWhiteSpace(newQualifierName)) throw new ArgumentException("NewQualifierName is required."); // ── 1. Load all source data in parallel ─────────────────── var sourceDefTask = _QualifierDAL.GetQualifierById(sourceQualifierId, loginDTO, ct); var ruleGroupsTask = _QualifierDAL.GetRuleGroups(sourceQualifierId, loginDTO, ct); var conditionsTask = _QualifierDAL.GetRuleConditions(sourceQualifierId, loginDTO, ct); var outputsTask = _QualifierDAL.GetQualifierOutputs(sourceQualifierId, loginDTO, ct); var methodsTask = _QualifierDAL.GetQualifierMethods(sourceQualifierId, loginDTO, ct); var dependenciesTask = _QualifierDAL.GetQualifierDependencies(sourceQualifierId, loginDTO, ct); await Task.WhenAll( sourceDefTask, ruleGroupsTask, conditionsTask, outputsTask, methodsTask, dependenciesTask); // Tasks are already complete after WhenAll — await is free (no blocking) var sourceDef = await sourceDefTask ?? throw new ArgumentException($"Qualifier id={sourceQualifierId} not found."); var ruleGroups = await ruleGroupsTask; var conditions = await conditionsTask; var outputs = await outputsTask; var methods = await methodsTask; var dependencies = await dependenciesTask; // ── 2. Allocate new IDs in batch (minimize round-trips) ─── var defAuto = await _AutoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.QUALIFIER, loginDTO); int newQualifierId = defAuto.StartNumber; var rgIdMap = new Dictionary(); // old RuleGroupId → new RuleGroupId if (ruleGroups.Count > 0) { var rgAuto = await _AutoNumber.GetNumberAsync( ruleGroups.Count, AUTONUMBERCONSTANT.QUALIFIERRULEGROUP, loginDTO); for (int i = 0; i < ruleGroups.Count; i++) rgIdMap[ruleGroups[i].RuleGroupId] = rgAuto.StartNumber + i; } int conditionBase = 0; if (conditions.Count > 0) { var condAuto = await _AutoNumber.GetNumberAsync( conditions.Count, AUTONUMBERCONSTANT.QUALIFIERRULECONDITION, loginDTO); conditionBase = condAuto.StartNumber; } int outputBase = 0; if (outputs.Count > 0) { var outAuto = await _AutoNumber.GetNumberAsync( outputs.Count, AUTONUMBERCONSTANT.QUALIFIEROUTPUT, loginDTO); outputBase = outAuto.StartNumber; } int methodBase = 0; if (methods.Count > 0) { var methAuto = await _AutoNumber.GetNumberAsync( methods.Count, AUTONUMBERCONSTANT.QUALIFIERMETHOD, loginDTO); methodBase = methAuto.StartNumber; } int depBase = 0; if (dependencies.Count > 0) { var depAuto = await _AutoNumber.GetNumberAsync( dependencies.Count, AUTONUMBERCONSTANT.QUALIFIERDEPENDENCY, loginDTO); depBase = depAuto.StartNumber; } // ── 3. Insert everything in a single transaction ────────── var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { // Definition var clonedDef = new QualifierDefinitionDTO { QualifierId = newQualifierId, QualifierCode = newQualifierCode, QualifierName = newQualifierName, QualifierType = sourceDef.QualifierType, EntityCode = sourceDef.EntityCode, TemplateId = sourceDef.TemplateId, IsSystem = 0, // clones are never system-level Description = sourceDef.Description, Status = 1, SortOrder = sourceDef.SortOrder, SourceType = sourceDef.SourceType }; await _QualifierDAL.SaveQualifierDefinition(clonedDef, loginDTO, trans, ct); // Rule groups — BFS to honour parent-before-child insert order. // The DB may enforce PARENTRULEGROUPID FK, so roots must be inserted first. var rgList = ruleGroups.ToList(); var remaining = new List( rgList.Where(g => g.ParentRuleGroupId != null)); var saveQueue = new Queue( rgList.Where(g => g.ParentRuleGroupId == null)); while (saveQueue.Count > 0) { var g = saveQueue.Dequeue(); await _QualifierDAL.SaveQualifierRuleGroup( new QualifierRuleGroupDTO { RuleGroupId = rgIdMap[g.RuleGroupId], QualifierId = newQualifierId, ParentRuleGroupId = g.ParentRuleGroupId.HasValue ? rgIdMap[g.ParentRuleGroupId.Value] : null, GroupOperator = g.GroupOperator, SortOrder = g.SortOrder, SourceType = g.SourceType }, loginDTO, trans, ct); // Enqueue children whose parent was just saved var children = remaining .Where(c => c.ParentRuleGroupId == g.RuleGroupId) .ToList(); foreach (var child in children) { saveQueue.Enqueue(child); remaining.Remove(child); } } // Any groups whose parent was not found (data integrity issue in source) // — detach by saving with null parent rather than failing the clone. foreach (var g in remaining) { await _QualifierDAL.SaveQualifierRuleGroup( new QualifierRuleGroupDTO { RuleGroupId = rgIdMap[g.RuleGroupId], QualifierId = newQualifierId, ParentRuleGroupId = null, GroupOperator = g.GroupOperator, SortOrder = g.SortOrder, SourceType = g.SourceType }, loginDTO, trans, ct); } // Rule conditions — remap RuleGroupId for (int i = 0; i < conditions.Count; i++) { var c = conditions[i]; await _QualifierDAL.SaveQualifierRuleCondition( new QualifierRuleConditionDTO { ConditionId = conditionBase + i, RuleGroupId = rgIdMap.TryGetValue(c.RuleGroupId, out var newRgId) ? newRgId : c.RuleGroupId, QualifierId = newQualifierId, FieldPath = c.FieldPath, ConditionOperator = c.ConditionOperator, ExpectedValue = c.ExpectedValue, Severity = c.Severity, ErrorCode = c.ErrorCode, ErrorMessage = c.ErrorMessage, SortOrder = c.SortOrder, SourceType = c.SourceType }, loginDTO, trans, ct); } // Outputs (no RuleGroupId to remap) for (int i = 0; i < outputs.Count; i++) { var o = outputs[i]; await _QualifierDAL.SaveQualifierOutput( new QualifierOutputDTO { OutputId = outputBase + i, QualifierId = newQualifierId, OutputKey = o.OutputKey, OutputScope = o.OutputScope, SourceExpression = o.SourceExpression, SortOrder = o.SortOrder, SourceType = o.SourceType }, loginDTO, trans, ct); } // Methods for (int i = 0; i < methods.Count; i++) { var m = methods[i]; await _QualifierDAL.SaveQualifierMethod( new QualifierMethodDTO { QualifierMethodId = methodBase + i, QualifierId = newQualifierId, ExecutionMode = m.ExecutionMode, Expression = m.Expression, QueryCode = m.QueryCode, ServiceCode = m.ServiceCode, ReturnType = m.ReturnType, SortOrder = m.SortOrder, SourceType = m.SourceType }, loginDTO, trans, ct); } // Dependencies — DependsOnQualifierId is an external reference; keep it as-is for (int i = 0; i < dependencies.Count; i++) { var d = dependencies[i]; await _QualifierDAL.SaveQualifierDependency( new QualifierDependencyDTO { DependencyId = depBase + i, QualifierId = newQualifierId, DependsOnQualifierId = d.DependsOnQualifierId, SourceType = d.SourceType }, loginDTO, trans, ct); } await _QueryExecutor.CommitAsync(trans); _Logger.LogInformation( "Qualifier '{SrcCode}' (id={SrcId}) cloned to '{NewCode}' (id={NewId}) " + "— {RgCount} rule groups, {CondCount} conditions, {OutCount} outputs " + "by user {UserId}", sourceDef.QualifierCode, sourceQualifierId, newQualifierCode, newQualifierId, ruleGroups.Count, conditions.Count, outputs.Count, loginDTO.UserId); return $"{SuccessResponse.SaveSuccessMessage} {newQualifierId}"; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } } }