using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using GB5Shared.DTO.Framework.Login; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using GB5Shared.Telemetry; using static GB5Shared.GB5Constant.Constant; namespace GB5Shared.ScanIdentifier { /// /// Generic Generate/Lookup/Retire/Reactivate/MarkPrinted engine over MSCANIDENTIFIER. /// No entity-specific code — existence checks go through IScanTargetResolverDispatcher, /// generation defaults come from MSCANTARGETCONFIG. Register once per host (same DI shape /// as AutoNumber/IQueryExecutor) via plain constructor injection — no assembly-scan needed /// for this class itself, only for the IScanTargetResolver implementations it delegates to. /// public class ScanIdentifierEngine : IScanIdentifierEngine { private readonly IQueryExecutor _qe; private readonly AutoNumber _autoNumber; private readonly IScanTargetResolverDispatcher _resolvers; public ScanIdentifierEngine(IQueryExecutor qe, AutoNumber autoNumber, IScanTargetResolverDispatcher resolvers) { _qe = qe; _autoNumber = autoNumber; _resolvers = resolvers; } public async Task GenerateAsync( GenerateScanIdentifierRequestDTO request, LoginDTO login, CancellationToken ct) { GB5Trace.Step("scanidentifier-generate-validate", new { request.EntityType, request.EntityId, request.Quantity }); var exists = await _resolvers.ExistsAsync(request.EntityType, request.EntityId, login, ct).ConfigureAwait(false); if (!exists) throw new InvalidOperationException($"{request.EntityType} '{request.EntityId}' not found."); var config = await GetEffectiveConfigAsync(request.EntityType, login, ct).ConfigureAwait(false); var quantity = request.Quantity <= 0 ? 1 : request.Quantity; if (quantity > 1 && !config.AllowMultiQty) throw new InvalidOperationException($"{request.EntityType} does not allow multi-quantity generation."); if (config.MaxQty is int maxQty && quantity > maxQty) throw new InvalidOperationException($"Quantity {quantity} exceeds the configured maximum of {maxQty} for {request.EntityType}."); var identifierType = request.IdentifierType ?? config.DefaultIdentifierType; var baseValue = BuildBaseValue(config, request.EntityType, request.EntityId, identifierType); GB5Trace.Step("scanidentifier-generate-save", new { request.EntityType, request.EntityId, quantity }); var allocation = await _autoNumber.GetNumberAsync(quantity, AUTONUMBERCONSTANT.SCANIDENTIFIER, login).ConfigureAwait(false); var suffixWidth = quantity.ToString().Length; var generated = new List(quantity); await using var tx = await _qe.BeginTransactionAsync(login).ConfigureAwait(false); try { for (var i = 0; i < quantity; i++) { var scanIdentifierId = allocation.StartNumber + i; var identifierValue = quantity == 1 ? baseValue : $"{baseValue}/{(i + 1).ToString().PadLeft(suffixWidth, '0')}"; await _qe.ExecuteAsync(login, ScanIdentifierEngineQB.INSERT_SCANIDENTIFIER, new { ScanIdentifierId = scanIdentifierId, IdentifierType = identifierType, IdentifierValue = identifierValue, request.EntityType, request.EntityId, UserId = login.UserId, OUId = login.WorkOUId, TenantId = login.ClientId }, tx, cancellationToken: ct).ConfigureAwait(false); generated.Add(new GeneratedScanIdentifierDTO { ScanIdentifierId = scanIdentifierId, IdentifierType = identifierType, IdentifierValue = identifierValue, EntityType = request.EntityType, EntityId = request.EntityId, ValidFrom = DateTime.UtcNow, Status = 1, TenantId = login.ClientId }); } await tx.CommitAsync(ct).ConfigureAwait(false); } catch (Exception ex) { await tx.RollbackAsync(ct).ConfigureAwait(false); await _autoNumber.RollbackAutoNumber(AUTONUMBERCONSTANT.SCANIDENTIFIER, allocation.StartNumber, login).ConfigureAwait(false); GB5Trace.MarkFailed("scanidentifier-generate-failed", ex); throw; } return new GenerateScanIdentifierResultDTO { Generated = generated }; } public async Task GetByValueAsync(string identifierValue, LoginDTO login, CancellationToken ct) { var rows = await _qe.QueryAsync(login, ScanIdentifierEngineQB.GET_BY_VALUE, new { IdentifierValue = identifierValue, TenantId = login.ClientId }, cancellationToken: ct).ConfigureAwait(false); return rows.FirstOrDefault(); } public async Task> GetListPagedAsync( string entityType, int statusFilter, string searchTerm, DateTime generatedFrom, int page, int pageSize, LoginDTO login, CancellationToken ct) { var param = new { TenantId = login.ClientId, EntityType = entityType ?? string.Empty, StatusFilter = statusFilter, // -1 All, 1 Active (VALIDTO IS NULL), 0 Inactive SearchTerm = searchTerm ?? string.Empty, GeneratedFrom = generatedFrom, Offset = (page - 1) * pageSize, PageSize = pageSize }; return await _qe.QueryPagedAsync(login, ScanIdentifierEngineQB.GET_LIST_PAGED, param) .ConfigureAwait(false); } public async Task RetireAsync(int scanIdentifierId, LoginDTO login, CancellationToken ct) { GB5Trace.Step("scanidentifier-retire", new { scanIdentifierId }); await _qe.ExecuteAsync(login, ScanIdentifierEngineQB.RETIRE_SCANIDENTIFIER, new { ScanIdentifierId = scanIdentifierId, UserId = login.UserId, TenantId = login.ClientId }, cancellationToken: ct).ConfigureAwait(false); } public async Task ReactivateAsync(int scanIdentifierId, LoginDTO login, CancellationToken ct) { GB5Trace.Step("scanidentifier-reactivate", new { scanIdentifierId }); await _qe.ExecuteAsync(login, ScanIdentifierEngineQB.REACTIVATE_SCANIDENTIFIER, new { ScanIdentifierId = scanIdentifierId, UserId = login.UserId, TenantId = login.ClientId }, cancellationToken: ct).ConfigureAwait(false); } public async Task MarkPrintedAsync(int scanIdentifierId, LoginDTO login, CancellationToken ct) { GB5Trace.Step("scanidentifier-mark-printed", new { scanIdentifierId }); await _qe.ExecuteAsync(login, ScanIdentifierEngineQB.MARK_PRINTED, new { ScanIdentifierId = scanIdentifierId, UserId = login.UserId, TenantId = login.ClientId }, cancellationToken: ct).ConfigureAwait(false); } public async Task> GetPrinterTypesAsync(LoginDTO login, CancellationToken ct) => await _qe.QueryAsync(login, ScanIdentifierEngineQB.GET_PRINTER_TYPES, cancellationToken: ct).ConfigureAwait(false); public async Task> GetLabelTypesAsync(LoginDTO login, CancellationToken ct) => await _qe.QueryAsync(login, ScanIdentifierEngineQB.GET_LABEL_TYPES, cancellationToken: ct).ConfigureAwait(false); public async Task PrintBatchAsync(PrintBatchRequestDTO request, LoginDTO login, CancellationToken ct) { GB5Trace.Step("scanidentifier-print-batch", new { request.LabelTypeGcmId, count = request.ScanIdentifierIds.Count }); var templateRows = await _qe.QueryAsync(login, ScanIdentifierEngineQB.GET_LABEL_TEMPLATE, new { GcmId = request.LabelTypeGcmId }, cancellationToken: ct).ConfigureAwait(false); var template = templateRows.FirstOrDefault() ?? throw new InvalidOperationException($"Label type {request.LabelTypeGcmId} has no stored dimensions (MLABELTEMPLATE)."); var rows = new List<(GeneratedScanIdentifierDTO, string, string)>(request.ScanIdentifierIds.Count); foreach (var id in request.ScanIdentifierIds) { var idRows = await _qe.QueryAsync(login, ScanIdentifierEngineQB.GET_BY_ID, new { ScanIdentifierId = id, TenantId = login.ClientId }, cancellationToken: ct).ConfigureAwait(false); var identifier = idRows.FirstOrDefault(); if (identifier is null) continue; var display = await _resolvers.GetDisplayAsync(identifier.EntityType, identifier.EntityId, login, ct).ConfigureAwait(false); rows.Add((identifier, display?.Code ?? identifier.EntityType, display?.Name ?? identifier.EntityId)); } var pdfBytes = ScanLabelPdfRenderer.RenderBatch(rows, template); foreach (var id in request.ScanIdentifierIds) await MarkPrintedAsync(id, login, ct).ConfigureAwait(false); return pdfBytes; } public Task GetEffectiveConfigAsync(string entityType, LoginDTO login, CancellationToken ct) => GetEffectiveConfigInternalAsync(entityType, login, ct); private async Task GetEffectiveConfigInternalAsync(string entityType, LoginDTO login, CancellationToken ct) { var rows = await _qe.QueryAsync(login, ScanIdentifierEngineQB.GET_TARGET_CONFIG, new { EntityType = entityType, TenantId = login.ClientId }, cancellationToken: ct).ConfigureAwait(false); // No MSCANTARGETCONFIG row yet for this EntityType — a resolver can be registered before // an admin ever configures it; fall back to conservative single-QR defaults rather than fail. return rows.FirstOrDefault() ?? new ScanTargetConfigDTO { EntityType = entityType, DefaultIdentifierType = 2, ValueScheme = 0, AllowMultiQty = false }; } private static string BuildBaseValue(ScanTargetConfigDTO config, string entityType, string entityId, byte identifierType) { if (config.ValueScheme == 1) { // OpaqueCode: no entity info encoded — the code only ever resolves via a MSCANIDENTIFIER lookup. return Guid.NewGuid().ToString("N")[..12].ToUpperInvariant(); } var prefix = string.IsNullOrWhiteSpace(config.Prefix) ? entityType : config.Prefix; return $"{prefix}:{entityId}"; } } }