using System.Globalization; using System.Linq; using System.Text.Json; using System.Text.RegularExpressions; using ComplianceBLL.Adapter; using ComplianceDAL.CustomCode.AdapterConfig; using ComplianceDAL.CustomCode.ComplianceArtifact; using ComplianceDAL.CustomCode.GSTEWayBill; using ComplianceDAL.DTO.ComplianceArtifact; using ComplianceDAL.DTO.GSTEWayBill; using GB5Shared.DTO.Framework.Login; using GB5Shared.Resource.GB5Solution; using GB5Shared.Validation; using Microsoft.Extensions.Logging; namespace ComplianceBLL.GSTEWayBill; public class GSTEWayBillBLL : IGSTEWayBillBLL { private readonly IGSTEWayBillDAL _dal; private readonly IEWayBillPayloadBuilderDAL _payloadBuilderDal; private readonly IComplianceArtifactDAL _artifactDal; private readonly IAdapterConfigDAL _adapterConfigDal; private readonly IComplianceAdapterFactory _adapterFactory; private readonly IValidation _validation; private readonly ILogger _logger; public GSTEWayBillBLL( IGSTEWayBillDAL dal, IEWayBillPayloadBuilderDAL payloadBuilderDal, IComplianceArtifactDAL artifactDal, IAdapterConfigDAL adapterConfigDal, IComplianceAdapterFactory adapterFactory, IValidation validation, ILogger logger) { _dal = dal; _payloadBuilderDal = payloadBuilderDal; _artifactDal = artifactDal; _adapterConfigDal = adapterConfigDal; _adapterFactory = adapterFactory; _validation = validation; _logger = logger; } public async Task GetEWayBillAsync(int eWayBillId, LoginDTO login, CancellationToken ct) { try { var dto = await _dal.GetEWayBillAsync(eWayBillId, login, ct).ConfigureAwait(false); return JsonSerializer.Serialize(dto); } catch (Exception ex) { _logger.LogError(ex, "E-Way Bill: GetEWayBillAsync failed for EWayBillId {EWayBillId}", eWayBillId); throw; } } public async Task GetEWayBillByDocumentAsync(int documentId, string documentType, LoginDTO login, CancellationToken ct) { try { var dto = await _dal.GetEWayBillByDocumentAsync(documentId, documentType, login, ct).ConfigureAwait(false); return JsonSerializer.Serialize(dto); } catch (Exception ex) { _logger.LogError(ex, "E-Way Bill: GetEWayBillByDocumentAsync failed for DocumentId {DocumentId}", documentId); throw; } } // Ported from GB4's EWayDAL.EwayBillOffline (Type 0 = Offline/SubmissionMode "Offline", Type 1 = // Upload/SubmissionMode "API") — built directly from ERP data via EWayBillPayloadQB, NOT the // E-Invoice canonical builder (different query, different payload shape entirely). GB4's // ClearTax-only branch (LoginDTO.ClientId == -1399999874) is intentionally not ported. public async Task GenerateEWayBillAsync(GenerateEWayBillRequestDTO request, LoginDTO login, CancellationToken ct) { try { await _validation.NotNull(request, nameof(request)); if (request.DocumentId == -1) throw new ArgumentException("DocumentId must not be equal to -1."); bool isOffline = string.Equals(request.SubmissionMode, "Offline", StringComparison.OrdinalIgnoreCase); var ewayBillOffLine = await _payloadBuilderDal.BuildEwayBillOffLineAsync(request.DocumentId, login, ct).ConfigureAwait(false); if (ewayBillOffLine is null || ewayBillOffLine.billLists.Count == 0) return JsonSerializer.Serialize(new { success = false, message = "There is no record found..Check document.." }); var billList = ewayBillOffLine.billLists[0]; // Apply any transport overrides from the request if (!string.IsNullOrWhiteSpace(request.TransporterGSTIN)) billList.transporterId = request.TransporterGSTIN; if (!string.IsNullOrWhiteSpace(request.VehicleNumber)) billList.vehicleNo = request.VehicleNumber; if (request.TransportMode > 0) billList.transMode = request.TransportMode; if (request.Distance > 0) billList.transDistance = request.Distance; // SubmissionMode "Offline" = Generate only (GB4 Type 0): return the JSON built straight from // the ERP query — no "already exists" check, no adapter/artifact lookup, nothing persisted. if (isOffline) { _logger.LogInformation("E-Way Bill generated (offline) for DocumentId {DocumentId}", request.DocumentId); return JsonSerializer.Serialize(new { success = true, message = ComplianceResource.EWayBillOfflineGenerated, ewayBillPayload = ewayBillOffLine }); } var existing = await _dal.GetEWayBillByDocumentAsync(request.DocumentId, request.DocumentType, login, ct).ConfigureAwait(false); if (existing is { Status: "Active" }) return JsonSerializer.Serialize(new { success = false, message = "E-Way Bill already exists for this document.", eWayBillId = existing.EWayBillId, ewbNumber = existing.EWBNumber }); var adapterConfig = await _adapterConfigDal.GetAdapterConfigByGSTIN(billList.userGstin, "GST_INDIA", login, ct).ConfigureAwait(false); if (adapterConfig is null) return JsonSerializer.Serialize(new { success = false, message = ComplianceResource.AdapterConfigNotFound }); var artifact = await _artifactDal.GetArtifactBySource(request.DocumentType, request.DocumentId, "EWayBill", login, ct).ConfigureAwait(false); int artifactId; if (artifact is null) { artifactId = await _artifactDal.SaveArtifact(new ComplianceArtifactDTO { ArtifactType = "EWayBill", Framework = "GST_INDIA", SourceEntityType = request.DocumentType, SourceEntityId = request.DocumentId, GSTIN = billList.userGstin, Status = "DRAFT", AttemptCount = 0, CreatedById = login.UserId }, login, ct).ConfigureAwait(false); } else { artifactId = artifact.ArtifactId; } var attemptNumber = (artifact?.AttemptCount ?? 0) + 1; // GB4's EwayBillListOnlineSubmitDTO conversion — same fields as billList, renamed // actFromStateCode/actToStateCode, fixed transactionType=4. var onlineSubmit = new EwayBillOnlineSubmitDTO { userGstin = billList.userGstin, supplyType = billList.supplyType, subSupplyType = billList.subSupplyType, subSupplyDesc = billList.subSupplyDesc, docType = billList.docType, docNo = billList.docNo, docDate = billList.docDate, transType = billList.transType, fromGstin = billList.fromGstin, fromTrdName = billList.fromTrdName, fromAddr1 = billList.fromAddr1, fromAddr2 = billList.fromAddr2, fromPlace = billList.fromPlace, fromPincode = billList.fromPincode, fromStateCode = billList.fromStateCode, actFromStateCode = billList.actualFromStateCode, toGstin = billList.toGstin, toTrdName = billList.toTrdName, toAddr1 = billList.toAddr1, toAddr2 = billList.toAddr2, toPlace = billList.toPlace, toPincode = billList.toPincode, toStateCode = billList.toStateCode, actToStateCode = billList.actualToStateCode, totalValue = billList.totalValue, cgstValue = billList.cgstValue, sgstValue = billList.sgstValue, igstValue = billList.igstValue, cessValue = billList.cessValue, TotNonAdvolVal = billList.TotNonAdvolVal, OthValue = billList.OthValue, totInvValue = billList.totInvValue, transMode = billList.transMode, transDistance = billList.transDistance, transporterName = billList.transporterName, transporterId = billList.transporterId, transDocNo = billList.transDocNo, transDocDate = billList.transDocDate, vehicleNo = billList.vehicleNo, vehicleType = billList.vehicleType, mainHsnCode = billList.mainHsnCode, itemList = billList.itemList }; var payloadJson = JsonSerializer.Serialize(onlineSubmit); await _artifactDal.SavePayload(new CompliancePayloadDTO { ArtifactId = artifactId, PayloadType = "Canonical", PayloadFormat = "JSON", Payload = JsonSerializer.Serialize(ewayBillOffLine), AttemptNumber = attemptNumber }, login, ct).ConfigureAwait(false); await _artifactDal.SavePayload(new CompliancePayloadDTO { ArtifactId = artifactId, PayloadType = "Transformed", PayloadFormat = "JSON", Payload = payloadJson, AttemptNumber = attemptNumber }, login, ct).ConfigureAwait(false); await _artifactDal.UpdateArtifactStatus(artifactId, "VALIDATED", attemptNumber, login.UserId, login, ct).ConfigureAwait(false); var adapter = _adapterFactory.GetAdapter(adapterConfig); var response = await adapter.GenerateEWayBillAsync(payloadJson, billList.userGstin, string.Empty, string.Empty, adapterConfig, ct).ConfigureAwait(false); await _artifactDal.SaveAudit(new ComplianceAuditDTO { ArtifactId = artifactId, Action = "API_CALL", RequestPayload = payloadJson, ResponsePayload = response.RawResponse, PreviousState = "VALIDATED", NewState = response.IsSuccess ? "CLEARED" : "REJECTED", ProviderCode = "GSTCafe", HttpStatusCode = response.HttpStatusCode, ReferenceNumber = response.ReferenceId, ErrorMessage = response.ErrorMessage, AttemptNumber = attemptNumber, UserId = login.UserId }, login, ct).ConfigureAwait(false); if (!response.IsSuccess) { await _artifactDal.UpdateArtifactStatus(artifactId, "REJECTED", attemptNumber, login.UserId, login, ct).ConfigureAwait(false); _logger.LogWarning("E-Way Bill generation failed for DocumentId {DocumentId}: {Error}", request.DocumentId, response.ErrorMessage); return JsonSerializer.Serialize(new { success = false, message = ComplianceResource.PortalError, errorCode = response.ErrorCode, errorMessage = response.ErrorMessage }); } await _artifactDal.SavePayload(new CompliancePayloadDTO { ArtifactId = artifactId, PayloadType = "Response", PayloadFormat = "JSON", Payload = response.RawResponse, AttemptNumber = attemptNumber }, login, ct).ConfigureAwait(false); // Matches GB4's ProcessEway parsing (EwayOutputResponseDTO; distance comes from the // "pincodes is (\d+)" pattern in the alert message, same regex GB4 uses). var ewbResult = ParseEwayResponse(response.RawResponse); var eWayBillId = await _dal.SaveEWayBillAsync(new GSTEWayBillDTO { ArtifactId = artifactId, DocumentId = request.DocumentId, DocumentType = request.DocumentType, DocumentNumber = billList.docNo, DocumentDate = ParseGb4DocDate(billList.docDate), EWBNumber = ewbResult.ewbNumber, GSTIN = billList.userGstin, FromGSTIN = billList.fromGstin, FromName = billList.fromTrdName, FromAddress = $"{billList.fromAddr1} {billList.fromAddr2}".Trim(), FromPincode = billList.fromPincode.ToString(), FromStateCode = billList.fromStateCode, ToGSTIN = billList.toGstin, ToName = billList.toTrdName, ToAddress = $"{billList.toAddr1} {billList.toAddr2}".Trim(), ToPincode = billList.toPincode.ToString(), ToStateCode = billList.toStateCode, TransporterGSTIN = billList.transporterId, TransporterName = billList.transporterName, TransportDocNumber = billList.transDocNo, VehicleNumber = billList.vehicleNo, VehicleType = billList.vehicleType, TransportMode = billList.transMode, Distance = ewbResult.distance, TotalValue = billList.totInvValue, TaxableValue = billList.totalValue, IGSTAmount = billList.igstValue, CGSTAmount = billList.cgstValue, SGSTAmount = billList.sgstValue, CessAmount = billList.cessValue, Status = "Active", ValidUpto = ewbResult.validUpto, SubmissionMode = request.SubmissionMode, ClientId = login.ClientId, DatabaseName = login.DatabaseName, CreatedById = login.UserId }, login, ct).ConfigureAwait(false); // Matches GB4's ProcessEway TMMHEAD update (EBNUMBER/EWAYDATE/EXCISEREGISTERLASTNO=distance/ // FORMSISSUEDDATE=validUpto) — TMMHEAD is read directly by other MM reports/documents. await _dal.UpdateTMMHeadEWayByDocumentIdAsync( request.DocumentId, ewbResult.ewbNumber, DateTime.Now.ToString("dd-MMM-yyyy", CultureInfo.InvariantCulture), ewbResult.distance.ToString(CultureInfo.InvariantCulture), ewbResult.validUpto?.ToString("dd-MMM-yyyy HH:mm", CultureInfo.InvariantCulture) ?? string.Empty, login, ct).ConfigureAwait(false); await _artifactDal.UpdateArtifactStatus(artifactId, "CLEARED", attemptNumber, login.UserId, login, ct).ConfigureAwait(false); _logger.LogInformation("E-Way Bill generated: EWBNumber={EWBNumber} DocumentId={DocumentId} EWayBillId={EWayBillId}", ewbResult.ewbNumber, request.DocumentId, eWayBillId); return JsonSerializer.Serialize(new { success = true, message = ComplianceResource.EWayBillGenerated, eWayBillId, ewbNumber = ewbResult.ewbNumber, validUpto = ewbResult.validUpto }); } catch (Exception ex) { _logger.LogError(ex, "E-Way Bill: GenerateEWayBillAsync failed for DocumentId {DocumentId}", request.DocumentId); throw; } } public async Task GenerateBulkEWayBillAsync(GenerateBulkEWayBillRequestDTO request, LoginDTO login, CancellationToken ct) { try { await _validation.NotNull(request, nameof(request)); if (request.DocumentIds is not { Count: > 0 }) return JsonSerializer.Serialize(new { success = false, message = "No document IDs provided." }); var results = new List(); foreach (var docId in request.DocumentIds) { ct.ThrowIfCancellationRequested(); var singleRequest = new GenerateEWayBillRequestDTO { DocumentId = docId, DocumentType = request.DocumentType, SubmissionMode = request.SubmissionMode }; var resultJson = await GenerateEWayBillAsync(singleRequest, login, ct).ConfigureAwait(false); results.Add(JsonSerializer.Deserialize(resultJson)!); } return JsonSerializer.Serialize(new { success = true, count = results.Count, results }); } catch (Exception ex) { _logger.LogError(ex, "E-Way Bill: GenerateBulkEWayBillAsync failed for DocumentType {DocumentType}", request.DocumentType); throw; } } // Ported from GB4's EWayDAL.EwayBillFromIRN(CriteriaDTO, LoginDTO): generates an e-way bill // directly off an already-generated IRN — a different GSP endpoint/response shape from the // regular GenerateEwayBill flow (GenerateEWayFromIRN, not GenerateEwayBill). GB4 has no Offline/ // Type branch here since it's inherently tied to an already-uploaded e-invoice, so it always // calls the portal. The actual GSP call + request/response log file reuses the existing // CallEWayServiceAsync inside GSTCafeAdapter — no separate log mechanism is built for this endpoint. public async Task GenerateEWayBillFromIRNAsync(GenerateEWayBillFromIRNRequestDTO request, LoginDTO login, CancellationToken ct) { try { await _validation.NotNull(request, nameof(request)); if (request.DocumentId == -1) throw new ArgumentException("DocumentId must not be equal to -1."); var irn = await _dal.GetTMMHeadIRNByDocumentIdAsync(request.DocumentId, login, ct).ConfigureAwait(false); if (string.IsNullOrEmpty(irn)) return JsonSerializer.Serialize(new { success = false, message = "IRN is not generated for this invoice, Pl generate first and do Eway.." }); var ewayBillOffLine = await _payloadBuilderDal.BuildEwayBillOffLineAsync(request.DocumentId, login, ct).ConfigureAwait(false); if (ewayBillOffLine is null || ewayBillOffLine.billLists.Count == 0) return JsonSerializer.Serialize(new { success = false, message = "There is no record found..Check document.." }); var billList = ewayBillOffLine.billLists[0]; var adapterConfig = await _adapterConfigDal.GetAdapterConfigByGSTIN(billList.userGstin, "GST_INDIA", login, ct).ConfigureAwait(false); if (adapterConfig is null) return JsonSerializer.Serialize(new { success = false, message = ComplianceResource.AdapterConfigNotFound }); // Same override GenerateEInvoiceAsync applies for e-invoice, and CancelEWayBillAsync // applies above: adapterConfig was resolved via a fixed reference OU // (GET_ADAPTER_CONFIG_BY_GSTIN), which can carry a different GSTIN than the e-way bill's // own userGstin — if they disagree, re-fetch config for that GSTIN specifically. Matches // GB4's GSTGovtSwitchFrameDAL.GenerateEWayFromIRN's UserGstIn-vs-default-GsTin check. if (billList.userGstin != adapterConfig.GSTIN) { var dynamicConfig = await _adapterConfigDal.GetAdapterConfigByGSTIN(billList.userGstin, "GST_INDIA", login, ct).ConfigureAwait(false); if (dynamicConfig is not null) adapterConfig = dynamicConfig; } // GB4's EWayFromIRNDTO — TransId/TransName/TransDocNo/TransDocDt are only set when present // ("URP"/"none" transporter values, and blank doc numbers, are left null exactly as GB4 does). var fromIrn = new EWayFromIRNDTO { Irn = irn, Distance = billList.transDistance, TransMode = billList.transMode.ToString(CultureInfo.InvariantCulture), VehNo = billList.vehicleNo, VehType = "R" }; if (!string.IsNullOrEmpty(billList.transporterId) && !string.Equals(billList.transporterId, "URP", StringComparison.OrdinalIgnoreCase)) fromIrn.TransId = billList.transporterId; if (!string.Equals(billList.transporterName, "none", StringComparison.OrdinalIgnoreCase)) fromIrn.TransName = billList.transporterName; if (!string.IsNullOrEmpty(billList.transDocNo)) { fromIrn.TransDocNo = billList.transDocNo; fromIrn.TransDocDt = billList.transDocDate; } var payloadJson = JsonSerializer.Serialize(fromIrn); var artifact = await _artifactDal.GetArtifactBySource(request.DocumentType, request.DocumentId, "EWayBill", login, ct).ConfigureAwait(false); int artifactId; if (artifact is null) { artifactId = await _artifactDal.SaveArtifact(new ComplianceArtifactDTO { ArtifactType = "EWayBill", Framework = "GST_INDIA", SourceEntityType = request.DocumentType, SourceEntityId = request.DocumentId, GSTIN = billList.userGstin, Status = "DRAFT", AttemptCount = 0, CreatedById = login.UserId }, login, ct).ConfigureAwait(false); } else { artifactId = artifact.ArtifactId; } var attemptNumber = (artifact?.AttemptCount ?? 0) + 1; await _artifactDal.SavePayload(new CompliancePayloadDTO { ArtifactId = artifactId, PayloadType = "Transformed", PayloadFormat = "JSON", Payload = payloadJson, AttemptNumber = attemptNumber }, login, ct).ConfigureAwait(false); await _artifactDal.UpdateArtifactStatus(artifactId, "VALIDATED", attemptNumber, login.UserId, login, ct).ConfigureAwait(false); var adapter = _adapterFactory.GetAdapter(adapterConfig); var response = await adapter.GenerateEWayBillFromIRNAsync(payloadJson, billList.userGstin, string.Empty, string.Empty, adapterConfig, ct).ConfigureAwait(false); await _artifactDal.SaveAudit(new ComplianceAuditDTO { ArtifactId = artifactId, Action = "API_CALL", RequestPayload = payloadJson, ResponsePayload = response.RawResponse, PreviousState = "VALIDATED", NewState = response.IsSuccess ? "CLEARED" : "REJECTED", ProviderCode = "GSTCafe", HttpStatusCode = response.HttpStatusCode, ErrorMessage = response.ErrorMessage, AttemptNumber = attemptNumber, UserId = login.UserId }, login, ct).ConfigureAwait(false); if (!response.IsSuccess) { await _artifactDal.UpdateArtifactStatus(artifactId, "REJECTED", attemptNumber, login.UserId, login, ct).ConfigureAwait(false); _logger.LogWarning("E-Way Bill (from IRN) generation failed for DocumentId {DocumentId}: {Error}", request.DocumentId, response.ErrorMessage); return JsonSerializer.Serialize(new { success = false, message = ComplianceResource.PortalError, errorMessage = response.ErrorMessage }); } await _artifactDal.SavePayload(new CompliancePayloadDTO { ArtifactId = artifactId, PayloadType = "Response", PayloadFormat = "JSON", Payload = response.RawResponse, AttemptNumber = attemptNumber }, login, ct).ConfigureAwait(false); // Matches GB4's ProcessEwayWayByIRN parsing (EwayFromIRNOutPutDTO): distance comes from the // digits inside infodtls[0].Desc when present, else falls back to the distance sent up in // the request (billList.transDistance) — same fallback GB4 uses. var result = ParseEwayFromIRNResponse(response.RawResponse, billList.transDistance); if (result is null) return JsonSerializer.Serialize(new { success = false, message = ComplianceResource.PortalError, errorMessage = "Unexpected response from GSP." }); var eWayBillId = await _dal.SaveEWayBillAsync(new GSTEWayBillDTO { ArtifactId = artifactId, DocumentId = request.DocumentId, DocumentType = request.DocumentType, DocumentNumber = billList.docNo, DocumentDate = ParseGb4DocDate(billList.docDate), EWBNumber = result.Value.ewbNumber, GSTIN = billList.userGstin, FromGSTIN = billList.fromGstin, FromName = billList.fromTrdName, FromAddress = $"{billList.fromAddr1} {billList.fromAddr2}".Trim(), FromPincode = billList.fromPincode.ToString(), FromStateCode = billList.fromStateCode, ToGSTIN = billList.toGstin, ToName = billList.toTrdName, ToAddress = $"{billList.toAddr1} {billList.toAddr2}".Trim(), ToPincode = billList.toPincode.ToString(), ToStateCode = billList.toStateCode, TransporterGSTIN = billList.transporterId, TransporterName = billList.transporterName, TransportDocNumber = billList.transDocNo, VehicleNumber = billList.vehicleNo, VehicleType = billList.vehicleType, TransportMode = billList.transMode, Distance = result.Value.distance, TotalValue = billList.totInvValue, TaxableValue = billList.totalValue, IGSTAmount = billList.igstValue, CGSTAmount = billList.cgstValue, SGSTAmount = billList.sgstValue, CessAmount = billList.cessValue, Status = "Active", ValidUpto = result.Value.validUpto, SubmissionMode = "API", ClientId = login.ClientId, DatabaseName = login.DatabaseName, CreatedById = login.UserId }, login, ct).ConfigureAwait(false); // Matches GB4's ProcessEwayWayByIRN TMMHEAD update — EWAYDATE/FORMSISSUEDDATE are written as // the GSP's own raw EwbDt/EwbValidTill strings, unlike the regular ProcessEway path which // reformats them. That distinction is deliberate in GB4 and kept literal here. await _dal.UpdateTMMHeadEWayByDocumentIdAsync( request.DocumentId, result.Value.ewbNumber, result.Value.ewbDate, result.Value.distance.ToString(CultureInfo.InvariantCulture), result.Value.validUptoRaw, login, ct).ConfigureAwait(false); await _artifactDal.UpdateArtifactStatus(artifactId, "CLEARED", attemptNumber, login.UserId, login, ct).ConfigureAwait(false); _logger.LogInformation("E-Way Bill generated from IRN: EWBNumber={EWBNumber} DocumentId={DocumentId} EWayBillId={EWayBillId}", result.Value.ewbNumber, request.DocumentId, eWayBillId); return JsonSerializer.Serialize(new { success = true, message = ComplianceResource.EWayBillGenerated, eWayBillId, ewbNumber = result.Value.ewbNumber }); } catch (Exception ex) { _logger.LogError(ex, "E-Way Bill: GenerateEWayBillFromIRNAsync failed for DocumentId {DocumentId}", request.DocumentId); throw; } } // Ported from GB4's EWayDAL.CancelEway(int Type, EwayCancelDTO, LoginDTO): // Type 1 (Online) calls the GSP cancel API and, only on success, clears TMMHEAD's EBNUMBER column. // Type 0 (Offline) never calls the portal — it just clears TMMHEAD's EBNUMBER directly, since an // Offline-generated e-way bill was never reported to the government in the first place. // GB5 branches on request.SubmissionMode (posted by the FE, same as GenerateEWayBillAsync) // instead of a separate Type parameter. The actual GSP call + request/response log file is left // to the existing CallEWayServiceAsync inside GSTCafeAdapter.CancelEWayBillAsync — no separate // log mechanism is built for cancel. public async Task CancelEWayBillAsync(CancelEWayBillRequestDTO request, LoginDTO login, CancellationToken ct) { try { await _validation.NotNull(request, nameof(request)); if (request.EWayBillId <= 0) throw new ArgumentException("EWayBillId must be greater than zero."); await _validation.NotEmpty(request.EWBNumber, nameof(request.EWBNumber)); await _validation.NotEmpty(request.CancellationReason, nameof(request.CancellationReason)); var existing = await _dal.GetEWayBillAsync(request.EWayBillId, login, ct).ConfigureAwait(false); if (existing is null || existing.Status != "Active") return JsonSerializer.Serialize(new { success = false, message = "E-Way Bill not found or already cancelled." }); if (request.SubmissionMode == "Offline") { await _dal.ClearTMMHeadEBNumberAsync(request.EWBNumber, login, ct).ConfigureAwait(false); await _dal.CancelEWayBillAsync(request.EWayBillId, request.CancellationReason, login, ct).ConfigureAwait(false); await _artifactDal.UpdateArtifactStatus(existing.ArtifactId, "ARCHIVED", 0, login.UserId, login, ct).ConfigureAwait(false); _logger.LogInformation("E-Way Bill cancelled (Offline, no portal call): EWBNumber={EWBNumber} EWayBillId={EWayBillId}", request.EWBNumber, request.EWayBillId); return JsonSerializer.Serialize(new { success = true, message = ComplianceResource.EWayBillCancelled }); } var adapterConfig = await _adapterConfigDal.GetAdapterConfigByGSTIN(existing.GSTIN, "GST_INDIA", login, ct).ConfigureAwait(false); if (adapterConfig is null) return JsonSerializer.Serialize(new { success = false, message = ComplianceResource.AdapterConfigNotFound }); // Matches GB4's EWayDAL.CancelEway UserGstin check, applied the same way // GenerateEInvoiceAsync applies it for e-invoice: resolve the GSTIN currently registered // against the e-way bill's OU (or store) fresh (by EBNUMBER), and if it disagrees with the // resolved adapterConfig's own GSTIN, re-fetch config for that GSTIN specifically. // // TODO: GB4 picks OU-vs-Store lookup by two hardcoded ClientIds (WB/Sunbeam) — ported // literally for now per explicit instruction; this discriminator itself is expected to // change soon, at which point this ClientId check should be replaced. var isStoreBasedClient = login.ClientId == -1399999740 || login.ClientId == -1399999737; var userGstin = isStoreBasedClient ? await _dal.GetEwayGstinBasedOnStoreAsync(request.EWBNumber, login, ct).ConfigureAwait(false) : await _dal.GetEwayGstinBasedOnOuAsync(request.EWBNumber, login, ct).ConfigureAwait(false); if (string.IsNullOrEmpty(userGstin)) return JsonSerializer.Serialize(new { success = false, message = isStoreBasedClient ? "Gstin is not found based on Store.." : "Gstin is not found based on Ou.." }); if (userGstin != adapterConfig.GSTIN) { var dynamicConfig = await _adapterConfigDal.GetAdapterConfigByGSTIN(userGstin, "GST_INDIA", login, ct).ConfigureAwait(false); if (dynamicConfig is not null) adapterConfig = dynamicConfig; } var adapter = _adapterFactory.GetAdapter(adapterConfig); var response = await adapter.CancelEWayBillAsync(request.EWBNumber, request.CancelReasonType, request.CancellationReason, existing.GSTIN, string.Empty, string.Empty, adapterConfig, ct).ConfigureAwait(false); await _artifactDal.SaveAudit(new ComplianceAuditDTO { ArtifactId = existing.ArtifactId, Action = "API_CALL", RequestPayload = JsonSerializer.Serialize(request), ResponsePayload = response.RawResponse, PreviousState = "CLEARED", NewState = response.IsSuccess ? "ARCHIVED" : "CLEARED", ProviderCode = "GSTCafe", HttpStatusCode = response.HttpStatusCode, ErrorMessage = response.ErrorMessage, AttemptNumber = 1, UserId = login.UserId }, login, ct).ConfigureAwait(false); if (!response.IsSuccess) return JsonSerializer.Serialize(new { success = false, message = ComplianceResource.PortalError, errorMessage = response.ErrorMessage }); await _dal.ClearTMMHeadEBNumberAsync(request.EWBNumber, login, ct).ConfigureAwait(false); await _dal.CancelEWayBillAsync(request.EWayBillId, request.CancellationReason, login, ct).ConfigureAwait(false); await _artifactDal.UpdateArtifactStatus(existing.ArtifactId, "ARCHIVED", 0, login.UserId, login, ct).ConfigureAwait(false); _logger.LogInformation("E-Way Bill cancelled: EWBNumber={EWBNumber} EWayBillId={EWayBillId}", request.EWBNumber, request.EWayBillId); return JsonSerializer.Serialize(new { success = true, message = ComplianceResource.EWayBillCancelled }); } catch (Exception ex) { _logger.LogError(ex, "E-Way Bill: CancelEWayBillAsync failed for EWayBillId {EWayBillId}, EWBNumber {EWBNumber}", request.EWayBillId, request.EWBNumber); throw; } } public async Task UpdateVehicleAsync(UpdateVehicleRequestDTO request, LoginDTO login, CancellationToken ct) { try { await _validation.NotNull(request, nameof(request)); if (request.EWayBillId <= 0) throw new ArgumentException("EWayBillId must be greater than zero."); await _validation.NotEmpty(request.EWBNumber, nameof(request.EWBNumber)); await _validation.NotEmpty(request.VehicleNumber, nameof(request.VehicleNumber)); var existing = await _dal.GetEWayBillAsync(request.EWayBillId, login, ct).ConfigureAwait(false); if (existing is null || existing.Status != "Active") return JsonSerializer.Serialize(new { success = false, message = "E-Way Bill not found or not active." }); var adapterConfig = await _adapterConfigDal.GetAdapterConfigByGSTIN(existing.GSTIN, "GST_INDIA", login, ct).ConfigureAwait(false); if (adapterConfig is null) return JsonSerializer.Serialize(new { success = false, message = ComplianceResource.AdapterConfigNotFound }); var vehiclePayload = new { ewbNo = request.EWBNumber, vehicleNo = request.VehicleNumber, fromPlace = request.FromPlace, fromState = string.IsNullOrWhiteSpace(request.FromStateCode) ? existing.FromStateCode : request.FromStateCode, reasonCode = request.Reason, transDocNo = request.TransportDocNumber, transDocDate = request.TransportDocDate?.ToString("dd/MM/yyyy") ?? string.Empty, transMode = request.TransportMode, vehicleType = request.VehicleType }; var payloadJson = JsonSerializer.Serialize(vehiclePayload); var adapter = _adapterFactory.GetAdapter(adapterConfig); var response = await adapter.UpdateVehicleAsync(payloadJson, existing.GSTIN, string.Empty, string.Empty, adapterConfig, ct).ConfigureAwait(false); await _artifactDal.SaveAudit(new ComplianceAuditDTO { ArtifactId = existing.ArtifactId, Action = "API_CALL", RequestPayload = payloadJson, ResponsePayload = response.RawResponse, PreviousState = "CLEARED", NewState = response.IsSuccess ? "CLEARED" : "CLEARED", ProviderCode = "GSTCafe", HttpStatusCode = response.HttpStatusCode, ErrorMessage = response.ErrorMessage, AttemptNumber = 1, UserId = login.UserId }, login, ct).ConfigureAwait(false); if (!response.IsSuccess) return JsonSerializer.Serialize(new { success = false, message = ComplianceResource.PortalError, errorMessage = response.ErrorMessage }); await _dal.UpdateVehicleAsync(request.EWayBillId, request, login.UserId, login, ct).ConfigureAwait(false); _logger.LogInformation("Vehicle updated for EWBNumber={EWBNumber} EWayBillId={EWayBillId}", request.EWBNumber, request.EWayBillId); return JsonSerializer.Serialize(new { success = true, message = ComplianceResource.EWayBillVehicleUpdated }); } catch (Exception ex) { _logger.LogError(ex, "E-Way Bill: UpdateVehicleAsync failed for EWayBillId {EWayBillId}, EWBNumber {EWBNumber}", request.EWayBillId, request.EWBNumber); throw; } } public async Task GetEWBCoverageAsync(string gstin, string returnPeriod, LoginDTO login, CancellationToken ct) { try { var list = await _dal.GetCoverageListAsync(gstin, returnPeriod, login, ct).ConfigureAwait(false); return JsonSerializer.Serialize(list); } catch (Exception ex) { _logger.LogError(ex, "E-Way Bill: GetEWBCoverageAsync failed for GSTIN {GSTIN}, ReturnPeriod {ReturnPeriod}", gstin, returnPeriod); throw; } } // ── Response / date parsing ───────────────────────────────────────────────── private static DateTime ParseGb4DocDate(string docDate) => DateTime.TryParseExact(docDate, "dd/MM/yyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt) ? dt : DateTime.MinValue; // Matches GB4's EWayDAL.ProcessEway exactly: validUpto parsed as "dd/MM/yyyy hh:mm:ss tt", distance // extracted from the alert message via the "pincodes is (\d+)" pattern. private static (string ewbNumber, int distance, DateTime? validUpto) ParseEwayResponse(string rawResponse) { try { var result = JsonSerializer.Deserialize(rawResponse); if (result is { status: 1, data: not null }) { DateTime? validUpto = DateTime.TryParseExact( result.data.validUpto, "dd/MM/yyyy hh:mm:ss tt", CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsedValidUpto) ? parsedValidUpto : null; var match = Regex.Match(result.data.alert ?? string.Empty, @"pincodes is (\d+)"); var distance = match.Success ? int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture) : 0; return (result.data.ewayBillNo, distance, validUpto); } } catch { // Best-effort — malformed/unexpected response shape falls through to the empty result below. } return (string.Empty, 0, null); } // Matches GB4's ProcessEwayWayByIRN parsing (EwayFromIRNOutPutDTO) — distance comes from the // digits inside infodtls[0].Desc when present, else the caller-supplied fallback (the same // distance value GB4 itself received as a parameter, sourced from the request payload). // EwbDt/EwbValidTill are returned as-is (raw GSP strings) for the TMMHEAD write, exactly like // GB4 — unlike the regular ProcessEway, GB4 never reparses these two fields here. PropertyNameCaseInsensitive // matches Newtonsoft's default case-insensitive behavior GB4's JsonDeserializerString relied on. private static (string ewbNumber, string ewbDate, string validUptoRaw, int distance, DateTime? validUpto)? ParseEwayFromIRNResponse(string rawResponse, int fallbackDistance) { try { var result = JsonSerializer.Deserialize(rawResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (result is not { status_cd: 1, response_data: not null }) return null; var distance = fallbackDistance; if (result.infodtls is { Count: > 0 }) { var digitsOnly = new string(result.infodtls[0].Desc.Where(char.IsDigit).ToArray()); distance = string.IsNullOrEmpty(digitsOnly) ? 0 : int.Parse(digitsOnly, CultureInfo.InvariantCulture); } var validUpto = DateTime.TryParse(result.response_data.EwbValidTill, CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsed) ? parsed : (DateTime?)null; return (result.response_data.EwbNo.ToString(CultureInfo.InvariantCulture), result.response_data.EwbDt, result.response_data.EwbValidTill, distance, validUpto); } catch { return null; } } }