using System.Text.Json; using GB5Shared.DaprCache; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.Qualifier; using GB5Shared.EntityHandler; using GB5Shared.GB5Library.Qualifier; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using QMSDAL.CustomCode.PcsCard; using QMSDAL.CustomCode.PcsCardMaterial; using QMSDAL.CustomCode.PcsCardStage; using QMSDAL.DTO; using static GB5Shared.GB5Constant.Constant; namespace QMSBLL.PcsCard { // ───────────────────────────────────────────────────────────────────────── // PcsCardBLL // // Save pipeline: // 1. Validate serial number uniqueness // 2. BeginTransaction // 3. (if new) AutoNumber → CardNo format = "PCS/{Year}/{id:D5}" // 4. Load routing detail rows, filter by SKU variant // 5. Build stage list, set TotalStages // 6. ExecuteSaveAsync: SavePcsCardAsync + InsertManyAsync (all stages) // 7. CommitAsync + KeyInvalidate // // Delete guard: card must be OverallStatus=0 (not started). // ───────────────────────────────────────────────────────────────────────── public class PcsCardBLL : IPcsCardBLL { private readonly IPcsCardDAL _PcsCardDAL; private readonly IPcsCardStageDAL _PcsCardStageDAL; private readonly IPcsCardMaterialDAL _PcsCardMaterialDAL; private readonly AutoNumber _AutoNumber; private readonly IQueryExecutor _QueryExecutor; private readonly KeyInvalidate _KeyInvalidate; private readonly BaseEntityAppService _BaseEntityAppService; public PcsCardBLL( IPcsCardDAL pcsCardDAL, IPcsCardStageDAL pcsCardStageDAL, IPcsCardMaterialDAL pcsCardMaterialDAL, AutoNumber autoNumber, IQueryExecutor queryExecutor, KeyInvalidate keyInvalidate, BaseEntityAppService baseEntityAppService) { _PcsCardDAL = pcsCardDAL; _PcsCardStageDAL = pcsCardStageDAL; _PcsCardMaterialDAL = pcsCardMaterialDAL; _AutoNumber = autoNumber; _QueryExecutor = queryExecutor; _KeyInvalidate = keyInvalidate; _BaseEntityAppService = baseEntityAppService; } // ── Get single card ─────────────────────────────────────────────────── public async Task GetPcsCardAsync( int pcsCardId, LoginDTO login, CancellationToken ct = default) { try { return await _PcsCardDAL.GetPcsCardAsync(pcsCardId, login, ct).ConfigureAwait(false); } catch (Exception) { throw; } } public async Task GetPcsCardBySerialNoAsync( string serialNo, LoginDTO login, CancellationToken ct = default) { try { return await _PcsCardDAL.GetPcsCardBySerialNoAsync(serialNo, login, ct).ConfigureAwait(false); } catch (Exception) { throw; } } // ── Paged list ──────────────────────────────────────────────────────── public async Task<(IEnumerable Rows, int TotalCount)> GetPcsCardListAsync( int ouId, int periodId, byte? overallStatus, int? customerId, int? itemId, string? variantCode, string? search, DateTime? plannedEndFrom, DateTime? plannedEndTo, string sortBy, string sortDir, int pageOffset, int pageSize, LoginDTO login, CancellationToken ct = default) { try { return await _PcsCardDAL.GetPcsCardListAsync( ouId, periodId, overallStatus, customerId, itemId, variantCode, search, plannedEndFrom, plannedEndTo, sortBy, sortDir, pageOffset, pageSize, login, ct) .ConfigureAwait(false); } catch (Exception) { throw; } } // ── Full card — card + stages + materials loaded in parallel ────────── public async Task GetPcsCardFullAsync( int pcsCardId, LoginDTO login, CancellationToken ct = default) { try { var cardTask = _PcsCardDAL.GetPcsCardAsync(pcsCardId, login, ct); var stagesTask = _PcsCardStageDAL.GetByCardIdAsync(pcsCardId, login, ct); var materialsTask = _PcsCardMaterialDAL.GetByCardIdAsync(pcsCardId, login, ct); await Task.WhenAll(cardTask, stagesTask, materialsTask).ConfigureAwait(false); var card = await cardTask; if (card == null) return null; return new PcsCardFullDTO { Card = card, Stages = await stagesTask, Materials = await materialsTask }; } catch (Exception) { throw; } } // ── Save (insert or update) ─────────────────────────────────────────── public async Task SavePcsCardAsync( PcsCardDTO dto, LoginDTO login, CancellationToken ct = default) { if (dto == null) throw new ArgumentNullException(nameof(dto)); // Validate required fields if (string.IsNullOrWhiteSpace(dto.SerialNo) || dto.SerialNo.Length > 60) return "Serial number is required and must not exceed 60 characters."; if (dto.CustomerId == 0) return "Customer is required."; if (dto.ItemId == 0) return "Item is required."; if (dto.SkuId == 0) return "SKU / Variant is required."; if (dto.RoutingId == 0) return "Routing is required."; // Serial number uniqueness check (only for new cards) if (dto.PcsCardId == 0) { var existing = await _PcsCardDAL .GetPcsCardBySerialNoAsync(dto.SerialNo, login, ct).ConfigureAwait(false); if (existing != null) return $"Serial number '{dto.SerialNo}' is already in use."; } else { // Routing cannot change on update var current = await _PcsCardDAL .GetPcsCardAsync(dto.PcsCardId, login, ct).ConfigureAwait(false); if (current != null && current.RoutingId != dto.RoutingId) return "Routing cannot be changed once the card has been created."; } var Trans = await _QueryExecutor.BeginTransactionAsync(login); try { bool isNew = dto.PcsCardId == 0; List? stagesToInsert = null; if (isNew) { var auto = await _AutoNumber .GetNumberAsync(1, AUTONUMBERCONSTANT.PCSCARD, login) .ConfigureAwait(false); dto.PcsCardId = auto.StartNumber; dto.CardNo = $"PCS/{DateTime.UtcNow.Year}/{dto.PcsCardId:D5}"; // Load routing details and filter to applicable stages for this SKU var routingDetails = await _PcsCardStageDAL .GetRoutingDetailForSkuAsync(dto.RoutingId, dto.RoutingVersionId, login, ct) .ConfigureAwait(false); var variantCode = dto.VariantCode ?? string.Empty; stagesToInsert = routingDetails .Where(rd => { if (string.IsNullOrWhiteSpace(rd.Variants)) return true; try { var variants = JsonSerializer.Deserialize(rd.Variants) ?? []; return variants.Length == 0 || variants.Any(v => string.Equals(v, variantCode, StringComparison.OrdinalIgnoreCase)); } catch { return true; // treat malformed JSON as "apply to all" } }) .Select((rd, idx) => new PcsCardStageDTO { PcsCardStaeId = auto.StartNumber + idx + 1, PcsCardId = dto.PcsCardId, RoutingDetailId = rd.RoutingDetailId, StageNo = (short)(idx + 1), StageCode = rd.StageCode, StageName = rd.StageName ?? string.Empty, StageGroup = rd.StageGroup ?? string.Empty, RequiresSupervisor = rd.RequiresSupervisor, RequiresMaterialLog= rd.RequiresMaterialLog, AllowQuantity = rd.AllowQuantity, ChecklistId = rd.ChecklistId, StageStatus = 0, StageResult = 0 }) .ToList(); dto.TotalStages = (short)stagesToInsert.Count; dto.CompletedStages = 0; dto.CurrentStage = 1; dto.OverallStatus = 0; dto.OverallResult = 0; } // Outbox/workflow pipeline skipped — MENTITY registration pending for OBJECTPCSCARD if (isNew) { await _PcsCardDAL.SavePcsCardAsync(dto, login, Trans, ct).ConfigureAwait(false); if (stagesToInsert?.Count > 0) await _PcsCardStageDAL.InsertManyAsync(stagesToInsert, login, Trans, ct).ConfigureAwait(false); } else { await _PcsCardDAL.UpdatePcsCardAsync(dto, login, Trans, ct).ConfigureAwait(false); } await _QueryExecutor.CommitAsync(Trans); var cacheKey = new CacheKeyGeneration().KeyGeneration( dto.PcsCardId, EntityConstant.OBJECTPCSCARD, CacheKeyLevel.CLIENT_LEVEL, login); await _KeyInvalidate.AllInvalidateCache(cacheKey); return isNew ? "Details saved successfully." : "Details updated successfully."; } catch (Exception) { await _QueryExecutor.RollbackAsync(Trans); throw; } } // ── Delete (soft — STATUS = 2) ──────────────────────────────────────── public async Task DeletePcsCardAsync( int pcsCardId, LoginDTO login, CancellationToken ct = default) { var card = await _PcsCardDAL.GetPcsCardAsync(pcsCardId, login, ct).ConfigureAwait(false); if (card == null) return "PCS card not found."; if (card.OverallStatus > 0) return "Cannot delete a card that has been started."; var Trans = await _QueryExecutor.BeginTransactionAsync(login); try { // Outbox/workflow pipeline skipped — MENTITY registration pending for OBJECTPCSCARD await _PcsCardDAL.DeletePcsCardAsync(pcsCardId, login, Trans, ct).ConfigureAwait(false); await _QueryExecutor.CommitAsync(Trans); var cacheKey = new CacheKeyGeneration().KeyGeneration( pcsCardId, EntityConstant.OBJECTPCSCARD, CacheKeyLevel.CLIENT_LEVEL, login); await _KeyInvalidate.AllInvalidateCache(cacheKey); return "Details deleted successfully."; } catch (Exception) { await _QueryExecutor.RollbackAsync(Trans); throw; } } } }