using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using FrameworkDAL.CustomCode.GOP; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.GOP; using GB5Shared.EntityHandler; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using GB5Shared.Resource.Response; using static GB5Shared.GB5Constant.Constant; namespace FrameworkBLL.GOP { // ============================================================ // MappingBLL — Mapping Engine authoring/CRUD. // // MMAPPINGDEFINITION/MMAPPINGVERSION/MMAPPINGNODE PKs are confirmed live // (sys.columns.is_identity = 0 against GB5DEMO) to be plain caller-supplied // INT PKs, not IDENTITY — so ids are assigned here via AutoNumber, same as // Qualifier's sub-entities, not left for the DB or the caller to supply. // // Save pattern (per CLAUDE.md): BeginTransactionAsync → GetAutoNumber → // ExecuteSaveAsync → CommitAsync, RollbackAsync in catch, always rethrow. // Deletes are soft (STATUS = 2) and do not route through ExecuteSaveAsync, // matching QualifierBLL's Delete* methods exactly. // ============================================================ public class MappingBLL : IMappingBLL { private readonly IMappingDAL _MappingDAL; private readonly ICanonicalModelDAL _CanonicalModelDAL; private readonly AutoNumber _AutoNumber; private readonly IQueryExecutor _QueryExecutor; private readonly BaseEntityAppService _DefinitionEntitySvc; private readonly BaseEntityAppService _VersionEntitySvc; private readonly BaseEntityAppService _NodeEntitySvc; private readonly IMappingEngineBLL _MappingEngineBLL; public MappingBLL( IMappingDAL mappingDAL, ICanonicalModelDAL canonicalModelDAL, AutoNumber autoNumber, IQueryExecutor queryExecutor, BaseEntityAppService definitionEntitySvc, BaseEntityAppService versionEntitySvc, BaseEntityAppService nodeEntitySvc, IMappingEngineBLL mappingEngineBLL) { _MappingDAL = mappingDAL; _CanonicalModelDAL = canonicalModelDAL; _AutoNumber = autoNumber; _QueryExecutor = queryExecutor; _DefinitionEntitySvc = definitionEntitySvc; _VersionEntitySvc = versionEntitySvc; _NodeEntitySvc = nodeEntitySvc; _MappingEngineBLL = mappingEngineBLL; } // ── Definition ──────────────────────────────────────────────────────────── public async Task SaveMappingDefinition( MappingDefinitionDTO dto, LoginDTO loginDTO, CancellationToken ct = default) { if (string.IsNullOrWhiteSpace(dto.MapperCode)) throw new ArgumentException("MapperCode is required."); if (string.IsNullOrWhiteSpace(dto.MapperName)) throw new ArgumentException("MapperName is required."); // Canonical model references are optional, but if set they must be real — // this is deliberately a light FK-existence check only; validating that // every MappingNode actually covers the canonical model's required fields // belongs at ActivateMappingVersion time (once all nodes for the version // exist) and is not implemented yet. if (dto.SourceCanonicalModelId is { } srcId && await _CanonicalModelDAL.GetById(srcId, loginDTO, ct) is null) throw new ArgumentException($"SourceCanonicalModelId {srcId} does not exist."); if (dto.TargetCanonicalModelId is { } tgtId && await _CanonicalModelDAL.GetById(tgtId, loginDTO, ct) is null) throw new ArgumentException($"TargetCanonicalModelId {tgtId} does not exist."); bool isNew = dto.MappingDefinitionId == 0; var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { if (isNew) { var auto = await _AutoNumber.GetNumberAsync( 1, AUTONUMBERCONSTANT.MAPPINGDEFINITION, loginDTO); dto.MappingDefinitionId = auto.StartNumber; } await _DefinitionEntitySvc.ExecuteSaveAsync( EntityConstant.MAPPINGDEFINITION, -1, // SAVEMAPPINGDEFINITIONEVENTTYPEID — not yet in compiled GB5Shared.dll dto, loginDTO, async tx => { if (isNew) await _MappingDAL.SaveMappingDefinition(dto, loginDTO, tx, ct).ConfigureAwait(false); else await _MappingDAL.UpdateMappingDefinition(dto, loginDTO, tx, ct).ConfigureAwait(false); return dto.MappingDefinitionId; }, null, BIZTRANSACTIONCLASSCONSTANT.MAPPINGDEFINITION, BizTransactionConstant.MAPPINGDEFINITION, trans, isNewEntity: isNew); await _QueryExecutor.CommitAsync(trans); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {dto.MappingDefinitionId}" : $"{SuccessResponse.UpdateSuccessMessage} {dto.MappingDefinitionId}"; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } public async Task GetMappingDefinitionById( int mappingDefinitionId, LoginDTO loginDTO, CancellationToken ct = default) => await _MappingDAL.GetMappingDefinitionById(mappingDefinitionId, loginDTO, ct).ConfigureAwait(false); public async Task> GetMappingDefinitionList( LoginDTO loginDTO, CancellationToken ct = default) => await _MappingDAL.GetMappingDefinitionList(loginDTO, ct).ConfigureAwait(false); public async Task> GetSelectListMapping( CriteriaDTO criteriaDTO, LoginDTO loginDTO, CancellationToken ct = default) => await _MappingDAL.GetSelectListMapping(criteriaDTO, loginDTO, ct).ConfigureAwait(false); public async Task DeleteMappingDefinition( int mappingDefinitionId, LoginDTO loginDTO, CancellationToken ct = default) { if (mappingDefinitionId == 0) throw new ArgumentException("MappingDefinitionId is required."); var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { await _MappingDAL.DeleteMappingDefinition(mappingDefinitionId, loginDTO.UserId, loginDTO, trans, ct); await _QueryExecutor.CommitAsync(trans); return SuccessResponse.DeleteSuccessMessage; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } // ── Version ─────────────────────────────────────────────────────────────── public async Task SaveMappingVersion( MappingVersionDTO dto, LoginDTO loginDTO, CancellationToken ct = default) { if (dto.MappingDefinitionId == 0) throw new ArgumentException("MappingDefinitionId is required."); if (string.IsNullOrWhiteSpace(dto.VersionLabel)) throw new ArgumentException("VersionLabel is required."); var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { var auto = await _AutoNumber.GetNumberAsync( 1, AUTONUMBERCONSTANT.MAPPINGVERSION, loginDTO); dto.MappingVersionId = auto.StartNumber; dto.VersionStatus = 0; // Draft — always starts as Draft await _VersionEntitySvc.ExecuteSaveAsync( EntityConstant.MAPPINGVERSION, -1, // SAVEMAPPINGVERSIONEVENTTYPEID — not yet in compiled GB5Shared.dll dto, loginDTO, async tx => { await _MappingDAL.SaveMappingVersion(dto, loginDTO, tx, ct).ConfigureAwait(false); return dto.MappingVersionId; }, new Dictionary { { "MappingDefinitionId", dto.MappingDefinitionId } }, BIZTRANSACTIONCLASSCONSTANT.MAPPINGVERSION, BizTransactionConstant.MAPPINGVERSION, trans, isNewEntity: true); await _QueryExecutor.CommitAsync(trans); return $"{SuccessResponse.SaveSuccessMessage} {dto.MappingVersionId}"; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } public async Task ActivateMappingVersion( int mappingVersionId, int mappingDefinitionId, LoginDTO loginDTO, CancellationToken ct = default) { if (mappingVersionId == 0) throw new ArgumentException("MappingVersionId is required."); if (mappingDefinitionId == 0) throw new ArgumentException("MappingDefinitionId is required."); var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { // Archive any currently Active version first (0 rows affected = no active version — fine). await _MappingDAL.ArchiveActiveVersion(mappingDefinitionId, loginDTO.UserId, loginDTO, trans, ct).ConfigureAwait(false); await _MappingDAL.ActivateVersion(mappingVersionId, loginDTO.UserId, loginDTO, trans, ct).ConfigureAwait(false); await _QueryExecutor.CommitAsync(trans); return SuccessResponse.UpdateSuccess; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } public async Task GetActiveVersionByDefinition( int mappingDefinitionId, LoginDTO loginDTO, CancellationToken ct = default) => await _MappingDAL.GetActiveVersionByDefinition(mappingDefinitionId, loginDTO, ct).ConfigureAwait(false); public async Task> GetVersionsByDefinition( int mappingDefinitionId, LoginDTO loginDTO, CancellationToken ct = default) => await _MappingDAL.GetVersionsByDefinition(mappingDefinitionId, loginDTO, ct).ConfigureAwait(false); public async Task DeleteMappingVersion( int mappingVersionId, LoginDTO loginDTO, CancellationToken ct = default) { var status = await _MappingDAL.GetMappingVersionStatus(mappingVersionId, loginDTO, ct); if (status is null) throw new ArgumentException($"MappingVersion {mappingVersionId} 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 _MappingDAL.DeleteMappingVersion(mappingVersionId, loginDTO.UserId, loginDTO, trans, ct); await _QueryExecutor.CommitAsync(trans); return SuccessResponse.DeleteSuccessMessage; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } // ── Node ────────────────────────────────────────────────────────────────── public async Task SaveMappingNode( MappingNodeDTO dto, LoginDTO loginDTO, CancellationToken ct = default) { if (dto.MappingVersionId == 0) throw new ArgumentException("MappingVersionId is required."); if (string.IsNullOrWhiteSpace(dto.SourcePath)) throw new ArgumentException("SourcePath is required."); if (string.IsNullOrWhiteSpace(dto.TargetPath)) throw new ArgumentException("TargetPath is required."); bool isNew = dto.MappingNodeId == 0; var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { if (isNew) { var auto = await _AutoNumber.GetNumberAsync( 1, AUTONUMBERCONSTANT.MAPPINGNODE, loginDTO); dto.MappingNodeId = auto.StartNumber; } await _NodeEntitySvc.ExecuteSaveAsync( EntityConstant.MAPPINGNODE, -1, // SAVEMAPPINGNODEEVENTTYPEID — not yet in compiled GB5Shared.dll dto, loginDTO, async tx => { if (isNew) await _MappingDAL.SaveMappingNode(dto, loginDTO, tx, ct).ConfigureAwait(false); else await _MappingDAL.UpdateMappingNode(dto, loginDTO, tx, ct).ConfigureAwait(false); return dto.MappingNodeId; }, new Dictionary { { "MappingVersionId", dto.MappingVersionId } }, BIZTRANSACTIONCLASSCONSTANT.MAPPINGNODE, BizTransactionConstant.MAPPINGNODE, trans, isNewEntity: isNew); await _QueryExecutor.CommitAsync(trans); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {dto.MappingNodeId}" : $"{SuccessResponse.UpdateSuccessMessage} {dto.MappingNodeId}"; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } public async Task> GetNodesByVersion( int mappingVersionId, LoginDTO loginDTO, CancellationToken ct = default) => await _MappingDAL.GetNodesByVersion(mappingVersionId, loginDTO, ct).ConfigureAwait(false); public async Task DeleteMappingNode( int mappingNodeId, LoginDTO loginDTO, CancellationToken ct = default) { if (mappingNodeId == 0) throw new ArgumentException("MappingNodeId is required."); var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { await _MappingDAL.DeleteMappingNode(mappingNodeId, loginDTO, trans, ct); await _QueryExecutor.CommitAsync(trans); return SuccessResponse.DeleteSuccessMessage; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } // ── Test transform (Mapping Wizard, Step 4) ───────────────────────────────── public async Task TestMappingTransform( string mapperCode, string samplePayloadJson, LoginDTO loginDTO, CancellationToken ct = default) => await _MappingEngineBLL.TransformAsync(mapperCode, samplePayloadJson, loginDTO, ct) .ConfigureAwait(false); } }