using System.Text.RegularExpressions;
using CashLineERP.Application.Customers;
using CashLineERP.Application.External;
using CashLineERP.Application.Finance;
using CashLineERP.Application.Identity;
using CashLineERP.Application.Integration;
using CashLineERP.Application.Internal;
using CashLineERP.Application.Notifications;
using CashLineERP.Application.Parties;
using CashLineERP.Application.Procurement;
using CashLineERP.Application.Reporting;
using CashLineERP.Application.Sales;
using CashLineERP.Application.Suppliers;
using CashLineERP.Application.Tax;
using CashLineERP.Application.IslamicFinance.Disbursements;
using CashLineERP.Application.IslamicFinance.ProductProfiles;
using CashLineERP.Application.IslamicFinance.ReceivablesFinance;
using CashLineERP.Application.IslamicFinance.Requests;
using CashLineERP.Application.IslamicFinance.Settlements;
using CashLineERP.Contracts.Auth;
using CashLineERP.Contracts.Customers;
using CashLineERP.Contracts.External;
using CashLineERP.Contracts.Finance;
using CashLineERP.Contracts.Integration;
using CashLineERP.Contracts.Notifications;
using CashLineERP.Contracts.Parties;
using CashLineERP.Contracts.Procurement;
using CashLineERP.Contracts.Reporting;
using CashLineERP.Contracts.Sales;
using CashLineERP.Contracts.Suppliers;
using CashLineERP.Contracts.Tax;
using CashLineERP.Infrastructure.Persistence;
using CashLineERP.Infrastructure.Persistence.Repositories;
using CashLineERP.Infrastructure.Persistence.Repositories.IslamicFinance;
using CashLineERP.SharedKernel.Finance;

namespace CashLineERP.Api.Tests;

public sealed partial class ConnectedOperationalFlowsQaTests
{
    [Fact]
    public void Supplier_Public_Onboarding_Flow_Is_Connected_End_To_End()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var services = CreateHarness(dbContext);
        var lead = services.OnboardingLeadService.CreateLead(new CreatePublicOnboardingLeadRequest(
            "SUPPLIER",
            "Integrated Supplier Prospect",
            "Mariam Supplier",
            "integrated.supplier@example.com",
            "+201000009901",
            "Connected-flow supplier lead"));

        var application = services.PublicOnboardingService.CreateSupplierApplication(new CreatePublicSupplierOnboardingApplicationRequest(
            lead.LeadCode,
            "Integrated Supplier LLC",
            "EG",
            null,
            "TAX-SUP-INTEGRATED-001",
            "001",
            "integrated.supplier@example.com",
            "+201000009901",
            "Cairo",
            "LOCAL_VENDOR",
            "EGP",
            "BANK_TRANSFER",
            30,
            5,
            1500m,
            "Banque Misr",
            "4455667788991122",
            "BMISEGCX",
            "Connected supplier submission",
            [
                new RecordBusinessPartyComplianceDocumentRequest(
                    "COMMERCIAL_REGISTER",
                    "commercial-register.pdf",
                    "managed://connected/supplier/commercial-register.pdf",
                    "application/pdf",
                    2048)
            ]));

        var submittedLead = services.OnboardingLeadService.GetPublicLead(lead.LeadCode);
        Assert.NotNull(submittedLead);
        Assert.Equal("SELF_SERVICE_SUBMITTED", submittedLead!.StatusCode);

        var submittedCase = services.OnboardingCaseService.GetCase("SUPPLIER", application.SupplierProfileId);
        Assert.NotNull(submittedCase);
        Assert.Equal("PENDING_APPROVAL", submittedCase!.CaseStatusCode);
        Assert.Equal("ONBOARDING_APPROVAL", submittedCase.QueueCode);
        Assert.Equal("SUBMITTED", submittedCase.WorkflowStatusCode);

        services.SupplierProfileService.ApproveSupplierProfile(application.SupplierProfileId, 4);
        services.SupplierProfileService.VerifySupplierBank(application.SupplierProfileId, 4, new VerifySupplierBankRequest("BANK-VER-SUP-001"));
        services.SupplierProfileService.ActivateSupplierProfile(application.SupplierProfileId, 4);

        var activatedCase = services.OnboardingCaseService.GetCase("SUPPLIER", application.SupplierProfileId);
        Assert.NotNull(activatedCase);
        Assert.Equal("ACTIVATED", activatedCase!.CaseStatusCode);
        Assert.Equal("ONBOARDING_COMPLETED", activatedCase.QueueCode);
        Assert.True(activatedCase.IsActive);

        var provisionedUser = dbContext.Users.Single(entry => entry.PartyId == application.PartyId && entry.UserTypeCode == "EXTERNAL");
        var temporaryPassword = ExtractTemporaryPassword(dbContext.NotificationQueueItems
            .Where(entry => entry.RelatedEntityType == "SUPPLIER" && entry.RelatedEntityId == application.PartyId && entry.ChannelCode == "EMAIL")
            .OrderByDescending(entry => entry.Id)
            .Select(entry => entry.Body)
            .First());

        var login = services.IdentityService.Login(new LoginRequest(provisionedUser.LoginName, temporaryPassword, "external"));
        Assert.False(login.MfaRequired);

        var profile = services.IdentityService.GetCurrentUserProfile(provisionedUser.Id);
        Assert.Equal("supplier", profile.Workspace);
        Assert.Equal(application.PartyId, profile.PartyId);

        var auditActions = dbContext.AuditEvents
            .Where(entry => entry.EntityName == "SUPPLIER_PROFILE" && entry.EntityId == application.SupplierProfileId)
            .OrderBy(entry => entry.Id)
            .Select(entry => entry.ActionCode)
            .ToArray();
        Assert.Contains("SUBMITTED", auditActions);
        Assert.Contains("APPROVED", auditActions);
        Assert.Contains("ACTIVATED", auditActions);

        var outboxEvent = dbContext.OutboxEvents.Single(entry =>
            entry.AggregateType == "SUPPLIER_PROFILE" &&
            entry.AggregateId == application.SupplierProfileId &&
            entry.EventType == "SUPPLIER_PROFILE_ACTIVATED");
        Assert.Equal("PARTY", outboxEvent.SourceModuleCode);
    }

    [Fact]
    public void Customer_Public_Onboarding_Flow_Is_Connected_End_To_End()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var services = CreateHarness(dbContext);
        var lead = services.OnboardingLeadService.CreateLead(new CreatePublicOnboardingLeadRequest(
            "CUSTOMER",
            "Integrated Customer Prospect",
            "Omar Customer",
            "integrated.customer@example.com",
            "+201000009902",
            "Connected-flow customer lead"));

        var application = services.PublicOnboardingService.CreateCustomerApplication(new CreatePublicCustomerOnboardingApplicationRequest(
            lead.LeadCode,
            "Integrated Customer LLC",
            "EG",
            null,
            "TAX-CUS-INTEGRATED-001",
            "001",
            "integrated.customer@example.com",
            "+201000009902",
            "Alexandria",
            50000m,
            45,
            false,
            null,
            "COMPANY",
            "WHOLESALE",
            "EGP",
            "DIRECT_SALES",
            "STANDARD",
            "EMAIL",
            "Connected customer submission",
            [
                new RecordBusinessPartyComplianceDocumentRequest(
                    "VAT_CERTIFICATE",
                    "vat-certificate.pdf",
                    "managed://connected/customer/vat-certificate.pdf",
                    "application/pdf",
                    2048)
            ]));

        services.CustomerProfileService.ApproveCustomerProfile(application.CustomerProfileId, 4);
        services.CustomerProfileService.ActivateCustomerProfile(application.CustomerProfileId, 4);

        var activatedCase = services.OnboardingCaseService.GetCase("CUSTOMER", application.CustomerProfileId);
        Assert.NotNull(activatedCase);
        Assert.Equal("ACTIVATED", activatedCase!.CaseStatusCode);
        Assert.Equal("ONBOARDING_COMPLETED", activatedCase.QueueCode);
        Assert.True(activatedCase.IsActive);

        var provisionedUser = dbContext.Users.Single(entry => entry.PartyId == application.PartyId && entry.UserTypeCode == "EXTERNAL");
        var temporaryPassword = ExtractTemporaryPassword(dbContext.NotificationQueueItems
            .Where(entry => entry.RelatedEntityType == "CUSTOMER" && entry.RelatedEntityId == application.PartyId && entry.ChannelCode == "EMAIL")
            .OrderByDescending(entry => entry.Id)
            .Select(entry => entry.Body)
            .First());

        var login = services.IdentityService.Login(new LoginRequest(provisionedUser.LoginName, temporaryPassword, "external"));
        Assert.False(login.MfaRequired);

        var profile = services.IdentityService.GetCurrentUserProfile(provisionedUser.Id);
        Assert.Equal("customer", profile.Workspace);
        Assert.Equal(application.PartyId, profile.PartyId);

        var auditActions = dbContext.AuditEvents
            .Where(entry => entry.EntityName == "CUSTOMER_PROFILE" && entry.EntityId == application.CustomerProfileId)
            .OrderBy(entry => entry.Id)
            .Select(entry => entry.ActionCode)
            .ToArray();
        Assert.Contains("SUBMITTED", auditActions);
        Assert.Contains("APPROVED", auditActions);
        Assert.Contains("ACTIVATED", auditActions);

        var outboxEvent = dbContext.OutboxEvents.Single(entry =>
            entry.AggregateType == "CUSTOMER_PROFILE" &&
            entry.AggregateId == application.CustomerProfileId &&
            entry.EventType == "CUSTOMER_PROFILE_ACTIVATED");
        Assert.Equal("PARTY", outboxEvent.SourceModuleCode);
    }

    [Fact]
    public void Active_Party_Duplicate_Control_Blocks_Reused_Tax_Email_Or_Mobile_During_Onboarding()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var services = CreateHarness(dbContext);
        services.PublicOnboardingService.CreateSupplierApplication(new CreatePublicSupplierOnboardingApplicationRequest(
            null,
            "First Supplier LLC",
            "EG",
            null,
            "TAX-DUP-SUP-001",
            "001",
            "dup.supplier@example.com",
            "+201000009903",
            "Cairo",
            "LOCAL_VENDOR",
            "EGP",
            "BANK_TRANSFER",
            30,
            5,
            1000m,
            "Banque Misr",
            "9988776655443322",
            "BMISEGCX",
            "First duplicate baseline",
            []));

        var duplicateTax = Assert.Throws<InvalidOperationException>(() =>
            services.InternalOnboardingIntakeService.CreateSupplierApplication(new CreateInternalSupplierOnboardingRequest(
                "Second Supplier LLC",
                "EG",
                null,
                "TAX-DUP-SUP-001",
                "001",
                "new.supplier@example.com",
                "+201000009904",
                "Giza",
                "LOCAL_VENDOR",
                "EGP",
                "BANK_TRANSFER",
                30,
                5,
                1500m,
                "CIB",
                "1231231231239999",
                "CIBEEGCX",
                "Duplicate tax path"), 3));
        Assert.Contains("already exists", duplicateTax.Message, StringComparison.OrdinalIgnoreCase);

        var duplicateEmail = Assert.Throws<InvalidOperationException>(() =>
            services.InternalOnboardingIntakeService.CreateSupplierApplication(new CreateInternalSupplierOnboardingRequest(
                "Supplier With Duplicate Email",
                "EG",
                null,
                "TAX-DUP-SUP-002",
                "001",
                "dup.supplier@example.com",
                "+201000009905",
                "Alexandria",
                "LOCAL_VENDOR",
                "EGP",
                "BANK_TRANSFER",
                30,
                5,
                1000m,
                "CIB",
                "4564564564564564",
                "CIBEEGCX",
                "Duplicate email path"), 3));
        Assert.Contains("already exists", duplicateEmail.Message, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public void Supplier_Onboarding_Rework_Lifecycle_Remains_Synchronized_Across_Case_Audit_And_Approval_Visibility()
    {
        using var dbContext = QaDbContextFactory.CreateContext();
        var services = CreateHarness(dbContext);
        var internalWorkbenchService = new InternalWorkbenchService(new InternalWorkbenchRepository(dbContext));

        var application = services.PublicOnboardingService.CreateSupplierApplication(new CreatePublicSupplierOnboardingApplicationRequest(
            null,
            "Supplier Rework LLC",
            "EG",
            null,
            "TAX-SUP-REWORK-001",
            "001",
            "supplier.rework@example.com",
            "+201000009951",
            "Cairo",
            "LOCAL_VENDOR",
            "EGP",
            "BANK_TRANSFER",
            30,
            5,
            1200m,
            "Banque Misr",
            "5555666677778888",
            "BMISEGCX",
            "Supplier rework lifecycle",
            [
                new RecordBusinessPartyComplianceDocumentRequest(
                    "COMMERCIAL_REGISTER",
                    "commercial-register.pdf",
                    "managed://connected/supplier/rework-register.pdf",
                    "application/pdf",
                    2048)
            ]));

        var initialCase = services.OnboardingCaseService.GetCase("SUPPLIER", application.SupplierProfileId);
        Assert.NotNull(initialCase);
        Assert.Equal("PENDING_APPROVAL", initialCase!.CaseStatusCode);
        Assert.Equal("ONBOARDING_APPROVAL", initialCase.QueueCode);

        var approvalQueue = internalWorkbenchService.GetApprovalQueue();
        Assert.Contains(approvalQueue, item => item.DocumentType == "SUPPLIER_PROFILE" && item.DocumentId == application.SupplierProfileId);

        services.SupplierProfileService.HoldSupplierProfile(application.SupplierProfileId, 4, new HoldSupplierProfileRequest("Awaiting compliance clarification."));
        var heldCase = services.OnboardingCaseService.GetCase("SUPPLIER", application.SupplierProfileId);
        Assert.NotNull(heldCase);
        Assert.Equal("ON_HOLD", heldCase!.CaseStatusCode);
        Assert.Equal("ONBOARDING_HOLD", heldCase.QueueCode);
        Assert.True(heldCase.IsOnHold);

        services.SupplierProfileService.ReopenSupplierProfile(application.SupplierProfileId, 4, new ReopenSupplierProfileRequest("Supplier corrected the submitted package."));
        var reopenedCase = services.OnboardingCaseService.GetCase("SUPPLIER", application.SupplierProfileId);
        Assert.NotNull(reopenedCase);
        Assert.Equal("DRAFT", reopenedCase!.CaseStatusCode);
        Assert.Equal("ONBOARDING_DRAFT", reopenedCase.QueueCode);
        Assert.False(reopenedCase.IsOnHold);

        services.SupplierProfileService.SubmitSupplierProfile(application.SupplierProfileId, 3);
        services.SupplierProfileService.ReturnSupplierProfile(application.SupplierProfileId, 4, new ReviewSupplierProfileRequest("Please upload the updated legal document."));

        var returnedCase = services.OnboardingCaseService.GetCase("SUPPLIER", application.SupplierProfileId);
        Assert.NotNull(returnedCase);
        Assert.Equal("RETURNED_FOR_REWORK", returnedCase!.CaseStatusCode);
        Assert.Equal("ONBOARDING_EXCEPTION", returnedCase.QueueCode);

        var approvalQueueAfterReturn = internalWorkbenchService.GetApprovalQueue();
        Assert.DoesNotContain(approvalQueueAfterReturn, item => item.DocumentType == "SUPPLIER_PROFILE" && item.DocumentId == application.SupplierProfileId);

        var auditActions = dbContext.AuditEvents
            .Where(entry => entry.EntityName == "SUPPLIER_PROFILE" && entry.EntityId == application.SupplierProfileId)
            .OrderBy(entry => entry.Id)
            .Select(entry => entry.ActionCode)
            .ToArray();
        Assert.Contains("SUBMITTED", auditActions);
        Assert.Contains("ON_HOLD", auditActions);
        Assert.Contains("REOPENED", auditActions);
        Assert.Contains("RETURNED", auditActions);
    }

    [Fact]
    public void Customer_Onboarding_Rejection_Remains_Synchronized_Across_Case_Audit_And_Approval_Visibility()
    {
        using var dbContext = QaDbContextFactory.CreateContext();
        var services = CreateHarness(dbContext);
        var internalWorkbenchService = new InternalWorkbenchService(new InternalWorkbenchRepository(dbContext));

        var application = services.PublicOnboardingService.CreateCustomerApplication(new CreatePublicCustomerOnboardingApplicationRequest(
            null,
            "Customer Reject LLC",
            "EG",
            null,
            "TAX-CUS-REJECT-001",
            "001",
            "customer.reject@example.com",
            "+201000009952",
            "Alexandria",
            25000m,
            30,
            false,
            null,
            "COMPANY",
            "WHOLESALE",
            "EGP",
            "DIRECT_SALES",
            "STANDARD",
            "EMAIL",
            "Customer rejection lifecycle",
            [
                new RecordBusinessPartyComplianceDocumentRequest(
                    "VAT_CERTIFICATE",
                    "vat-certificate.pdf",
                    "managed://connected/customer/reject-vat.pdf",
                    "application/pdf",
                    2048)
            ]));

        var submittedCase = services.OnboardingCaseService.GetCase("CUSTOMER", application.CustomerProfileId);
        Assert.NotNull(submittedCase);
        Assert.Equal("PENDING_APPROVAL", submittedCase!.CaseStatusCode);
        Assert.Equal("ONBOARDING_APPROVAL", submittedCase.QueueCode);

        var approvalQueue = internalWorkbenchService.GetApprovalQueue();
        Assert.Contains(approvalQueue, item => item.DocumentType == "CUSTOMER_PROFILE" && item.DocumentId == application.CustomerProfileId);

        services.CustomerProfileService.RejectCustomerProfile(application.CustomerProfileId, 4, new ReviewCustomerProfileRequest("KYC screening result is not acceptable."));

        var rejectedCase = services.OnboardingCaseService.GetCase("CUSTOMER", application.CustomerProfileId);
        Assert.NotNull(rejectedCase);
        Assert.Equal("REJECTED", rejectedCase!.CaseStatusCode);
        Assert.Equal("ONBOARDING_EXCEPTION", rejectedCase.QueueCode);

        var approvalQueueAfterRejection = internalWorkbenchService.GetApprovalQueue();
        Assert.DoesNotContain(approvalQueueAfterRejection, item => item.DocumentType == "CUSTOMER_PROFILE" && item.DocumentId == application.CustomerProfileId);

        var auditActions = dbContext.AuditEvents
            .Where(entry => entry.EntityName == "CUSTOMER_PROFILE" && entry.EntityId == application.CustomerProfileId)
            .OrderBy(entry => entry.Id)
            .Select(entry => entry.ActionCode)
            .ToArray();
        Assert.Contains("SUBMITTED", auditActions);
        Assert.Contains("REJECTED", auditActions);
    }

    [Fact]
    public void Activated_Onboarding_Async_Artifacts_Appear_In_Internal_Operational_Dashboard()
    {
        using var dbContext = QaDbContextFactory.CreateContext();
        var services = CreateHarness(dbContext);
        var supplier = CreateAndActivateSupplier(services, "TAX-SUP-OPS-001", "supplier.ops@example.com", "+201000009953");
        var internalWorkbenchService = new InternalWorkbenchService(new InternalWorkbenchRepository(dbContext));

        var notification = dbContext.NotificationQueueItems
            .Where(entry => entry.RelatedEntityType == "SUPPLIER" && entry.RelatedEntityId == supplier.PartyId)
            .OrderByDescending(entry => entry.Id)
            .First();
        notification.MoveToDeadLetter("Operational visibility test notification dead-letter");

        var outboxEvent = dbContext.OutboxEvents
            .Where(entry => entry.AggregateType == "SUPPLIER_PROFILE" && entry.AggregateId == supplier.SupplierProfileId)
            .OrderByDescending(entry => entry.Id)
            .First();
        outboxEvent.MoveToDeadLetter("Operational visibility test outbox dead-letter");

        dbContext.SaveChanges();

        var dashboard = internalWorkbenchService.GetOperationalDashboard("Testing", "1.0.0.0");
        Assert.True(dashboard.DeadLetterNotificationCount >= 1);
        Assert.True(dashboard.DeadLetterOutboxCount >= 1);

        var criticalAlerts = internalWorkbenchService.GetOperationalAlerts("CRITICAL");
        Assert.Contains(criticalAlerts, item => item.AlertCode == "OUTBOX_DEAD_LETTER" && item.SourceArea == "INTEGRATION");
    }

    [Fact]
    public void Activated_Supplier_Flow_Connects_Procurement_To_AP_Statement_And_Reporting()
    {
        using var dbContext = QaDbContextFactory.CreateContext();
        var services = CreateHarness(dbContext);
        var supplier = CreateAndActivateSupplier(services, "TAX-SUP-FLOW-001", "supplier.flow@example.com", "+201000009911");

        var purchaseRequisitionService = new PurchaseRequisitionService(new PurchaseRequisitionRepository(dbContext));
        var rfqService = new RfqService(new RfqRepository(dbContext));
        var quotationService = new SupplierQuotationService(new SupplierQuotationRepository(dbContext));
        var evaluationService = new RfqEvaluationService(new RfqEvaluationRepository(dbContext));
        var goodsReceiptService = new GoodsReceiptService(new GoodsReceiptRepository(dbContext));
        var apBillService = new AccountsPayableBillService(new AccountsPayableBillRepository(dbContext));
        var apPaymentService = new AccountsPayablePaymentService(new AccountsPayablePaymentRepository(dbContext));
        var statementService = new SupplierStatementService(new SupplierStatementRepository(dbContext));
        var reconciliationService = new SupplierAccountReconciliationService(new SupplierAccountReconciliationRepository(dbContext));
        var reportingService = new CoreReportingService(new CoreReportingRepository(dbContext));

        var requisition = purchaseRequisitionService.CreatePurchaseRequisition(new CreatePurchaseRequisitionRequest(
            BranchId: 1,
            RequesterUserId: 1,
            RequisitionDate: new DateOnly(2026, 4, 24),
            NeedByDate: new DateOnly(2026, 4, 27),
            PurposeText: "Integrated supplier procurement flow",
            CurrencyCode: "EGP",
            Lines: [new PurchaseRequisitionLineRequest(1001, 6m, "EA", new DateOnly(2026, 4, 27), 25m, "Integrated supplier item")]));

        var submittedRequisition = purchaseRequisitionService.SubmitPurchaseRequisition(requisition.PurchaseRequisitionId, 1);
        var rfq = rfqService.CreateRfq(new CreateRfqRequest(
            BranchId: 1,
            DepartmentId: 3,
            PurchaseRequisitionId: submittedRequisition.PurchaseRequisitionId,
            RequesterUserId: 1,
            BuyerUserId: 2,
            RfqType: "STANDARD",
            RfqDate: new DateOnly(2026, 4, 24),
            ValidUntilDate: new DateOnly(2026, 4, 25),
            CurrencyCode: "EGP",
            Notes: "Integrated supplier RFQ",
            Lines:
            [
                new CreateRfqLineRequest(
                    submittedRequisition.Lines.First().LineId,
                    submittedRequisition.Lines.First().ItemId,
                    "Integrated supplier item",
                    6m,
                    "EA",
                    new DateOnly(2026, 4, 27),
                    "VAT14",
                    25m,
                    null)
            ],
            SupplierIds: [supplier.SupplierProfileId]));

        var submittedRfq = rfqService.SubmitRfq(rfq.RfqId, 2);
        var quotation = quotationService.CreateQuotation(new CreateSupplierQuotationRequest(
            RequestForQuotationId: submittedRfq.RfqId,
            SupplierId: supplier.SupplierProfileId,
            QuoteDate: new DateOnly(2026, 4, 24),
            ValidUntilDate: new DateOnly(2026, 4, 29),
            CurrencyCode: "EGP",
            FxRate: 1m,
            Notes: "Integrated supplier quote",
            Lines:
            [
                new CreateSupplierQuotationLineRequest(
                    submittedRfq.Lines.First().RfqLineId,
                    submittedRfq.Lines.First().ItemId,
                    6m,
                    "EA",
                    25m,
                    0m,
                    0m,
                    "VAT14",
                    1)
            ]));

        var submittedQuotation = quotationService.SubmitQuotation(quotation.SupplierQuotationId, 2);
        var evaluation = evaluationService.CreateEvaluation(new CreateRfqEvaluationRequest(
            RequestForQuotationId: submittedRfq.RfqId,
            EvaluationDate: new DateOnly(2026, 4, 24),
            Notes: "Integrated supplier evaluation",
            Lines:
            [
                new CreateRfqEvaluationLineRequest(
                    submittedQuotation.SupplierQuotationId,
                    submittedQuotation.SupplierId,
                    94m,
                    91m,
                    90m,
                    92m,
                    1,
                    true,
                    "Recommended")
            ]));

        evaluationService.FinalizeRecommendation(evaluation.RfqEvaluationId, 3);
        var purchaseOrder = evaluationService.AwardPurchaseOrder(evaluation.RfqEvaluationId, new AwardPurchaseOrderRequest(4, new DateOnly(2026, 4, 24), 1));

        var goodsReceipt = goodsReceiptService.PostGoodsReceipt(
            goodsReceiptService.CreateGoodsReceipt(new CreateGoodsReceiptRequest(
                BranchId: 1,
                PurchaseOrderId: purchaseOrder.PurchaseOrderId,
                SupplierId: purchaseOrder.SupplierId,
                ReceivedAtUtc: new DateTime(2026, 4, 25, 9, 0, 0, DateTimeKind.Utc),
                ReceivedByUserId: 4,
                InspectionStatus: "ACCEPTED",
                Lines:
                [
                    new CreateGoodsReceiptLineRequest(
                        purchaseOrder.Lines.First().PurchaseOrderLineId,
                        purchaseOrder.Lines.First().ItemId,
                        purchaseOrder.Lines.First().WarehouseId,
                        null,
                        6m,
                        6m,
                        0m,
                        "SUP-FLOW-GRN-1",
                        null)
                ])).GoodsReceiptId,
            4);

        var apBill = apBillService.CreateBill(new CreateAccountsPayableBillRequest(
            SupplierId: supplier.PartyId,
            PurchaseOrderId: purchaseOrder.PurchaseOrderId,
            GoodsReceiptId: goodsReceipt.GoodsReceiptId,
            BillDate: new DateOnly(2026, 4, 25),
            DueDate: new DateOnly(2026, 4, 30),
            CurrencyCode: "EGP",
            Lines:
            [
                new CreateAccountsPayableBillLineRequest(
                    purchaseOrder.Lines.First().PurchaseOrderLineId,
                    purchaseOrder.Lines.First().ItemId,
                    6m,
                    "EA",
                    25m,
                    "VAT14")
            ]));

        apBillService.SubmitBill(apBill.AccountsPayableBillId, 3);
        apBillService.ApproveMatch(apBill.AccountsPayableBillId, 4);
        var postedBill = apBillService.PostBill(apBill.AccountsPayableBillId, 4);

        var payment = apPaymentService.CreatePayment(new CreateAccountsPayablePaymentRequest(
            SupplierId: supplier.PartyId,
            PaymentDate: new DateOnly(2026, 4, 26),
            PaymentMethod: "BANK_TRANSFER",
            CurrencyCode: "EGP",
            AmountAmount: 60m,
            ReferenceNumber: "SUP-FLOW-PAY-001",
            Notes: "Integrated supplier partial settlement",
            Allocations:
            [
                new CreateAccountsPayablePaymentAllocationRequest(postedBill.AccountsPayableBillId, 60m, new DateOnly(2026, 4, 26))
            ]));
        apPaymentService.SubmitPayment(payment.AccountsPayablePaymentId, 5);
        apPaymentService.ApprovePayment(payment.AccountsPayablePaymentId, 6);
        var postedPayment = apPaymentService.PostPayment(payment.AccountsPayablePaymentId, 6);

        var statement = statementService.GenerateStatement(3, new CreateSupplierStatementRequest(
            supplier.PartyId,
            new DateOnly(2026, 4, 1),
            new DateOnly(2026, 4, 30),
            "EGP"));

        var reconciliation = reconciliationService.GetDetail(supplier.PartyId, new DateOnly(2026, 4, 30), "EGP");
        var report = reportingService.GetCoreFinanceReport(new DateOnly(2026, 4, 1), new DateOnly(2026, 4, 30));

        Assert.Contains(statement.Lines, line => line.DocumentNumber == postedBill.BillNumber);
        Assert.Contains(statement.Lines, line => line.DocumentNumber == postedPayment.PaymentNumber);
        Assert.Contains(reconciliation.Lines, line => line.DocumentNumber == postedBill.BillNumber);
        Assert.Contains(reconciliation.Lines, line => line.DocumentType == "AP_PAYMENT" && line.DocumentNumber == postedPayment.PaymentNumber);
        Assert.Contains(report.Rows, row => row.DocumentNumber == postedBill.BillNumber && row.ReportSection == "AP");
        Assert.Contains(report.Rows, row => row.DocumentNumber == postedPayment.PaymentNumber && row.DocumentType == "AP_PAYMENT");
    }

    [Fact]
    public void Activated_Customer_Flow_Connects_Sales_To_AR_Statement_And_Reporting()
    {
        using var dbContext = QaDbContextFactory.CreateContext();
        var services = CreateHarness(dbContext);
        var customer = CreateAndActivateCustomer(services, "TAX-CUS-FLOW-001", "customer.flow@example.com", "+201000009921");

        var salesOrderService = new SalesOrderService(new SalesOrderRepository(dbContext));
        var dispatchService = new DispatchOrderService(new DispatchOrderRepository(dbContext));
        var invoiceService = new AccountsReceivableInvoiceService(new AccountsReceivableInvoiceRepository(dbContext));
        var receiptService = new AccountsReceivableReceiptService(new AccountsReceivableReceiptRepository(dbContext));
        var statementService = new CustomerStatementService(new CustomerStatementRepository(dbContext));
        var reportingService = new CoreReportingService(new CoreReportingRepository(dbContext));

        var salesOrder = salesOrderService.CreateSalesOrder(new CreateSalesOrderRequest(
            BranchId: 1,
            CustomerId: customer.PartyId,
            OrderDate: new DateOnly(2026, 4, 10),
            CurrencyCode: "EGP",
            ExchangeRate: 1m,
            Lines: [new CreateSalesOrderLineRequest(1001, 1, 3m, "EA", 50m, "VAT14")]));

        var submittedSalesOrder = salesOrderService.SubmitSalesOrder(salesOrder.SalesOrderId, 2);
        var dispatch = dispatchService.PostDispatchOrder(
            dispatchService.CreateDispatchOrder(new CreateDispatchOrderRequest(
                BranchId: 1,
                SalesOrderId: submittedSalesOrder.SalesOrderId,
                DispatchWarehouseId: 1,
                PlannedShipAtUtc: new DateTime(2026, 4, 10, 11, 0, 0, DateTimeKind.Utc),
                Lines:
                [
                    new CreateDispatchOrderLineRequest(
                        submittedSalesOrder.Lines.First().SalesOrderLineId,
                        submittedSalesOrder.Lines.First().ItemId,
                        3m,
                        null,
                        "CUS-FLOW-DSP-1",
                        null)
                ])).DispatchOrderId,
            new PostDispatchOrderRequest(
                6,
                new DateTime(2026, 4, 10, 12, 0, 0, DateTimeKind.Utc),
                new DateTime(2026, 4, 10, 15, 0, 0, DateTimeKind.Utc),
                "Integrated receiver"));

        var postedInvoice = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostInvoice(
            invoiceService,
            invoiceService.CreateInvoice(new CreateAccountsReceivableInvoiceRequest(
                CustomerId: customer.PartyId,
                SalesOrderId: submittedSalesOrder.SalesOrderId,
                DispatchOrderId: dispatch.DispatchOrderId,
                InvoiceType: "TAX",
                InvoiceDate: new DateOnly(2026, 4, 10),
                TaxDate: new DateOnly(2026, 4, 10),
                DueDate: new DateOnly(2026, 4, 20),
                CurrencyCode: "EGP",
                Lines:
                [
                    new CreateAccountsReceivableInvoiceLineRequest(
                        submittedSalesOrder.Lines.First().SalesOrderLineId,
                        submittedSalesOrder.Lines.First().ItemId,
                        3m,
                        "EA",
                        50m,
                        "VAT14")
                ])).AccountsReceivableInvoiceId);

        var postedReceipt = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostReceipt(
            receiptService,
            receiptService.CreateReceipt(new CreateAccountsReceivableReceiptRequest(
                CustomerId: customer.PartyId,
                ReceiptDate: new DateOnly(2026, 4, 15),
                ReceiptMethod: "BANK_TRANSFER",
                CurrencyCode: "EGP",
                AmountAmount: 60m,
                ReferenceNumber: "CUS-FLOW-RCPT-001",
                Notes: "Integrated customer receipt",
                Allocations:
                [
                    new CreateAccountsReceivableReceiptAllocationRequest(postedInvoice.AccountsReceivableInvoiceId, 60m, new DateOnly(2026, 4, 15))
                ])).AccountsReceivableReceiptId);

        var statement = statementService.GenerateStatement(3, new CreateCustomerStatementRequest(
            customer.PartyId,
            new DateOnly(2026, 4, 1),
            new DateOnly(2026, 4, 30),
            "EGP"));
        var report = reportingService.GetCoreFinanceReport(new DateOnly(2026, 4, 1), new DateOnly(2026, 4, 30));

        Assert.Contains(statement.Lines, line => line.DocumentNumber == postedInvoice.InvoiceNumber);
        Assert.Contains(statement.Lines, line => line.DocumentNumber == postedReceipt.ReceiptNumber);
        Assert.Contains(report.Rows, row => row.DocumentNumber == postedInvoice.InvoiceNumber && row.ReportSection == "AR");
        Assert.Contains(report.Rows, row => row.DocumentNumber == postedReceipt.ReceiptNumber && row.DocumentType == "AR_RECEIPT");
    }

    [Fact]
    public void Credential_Notification_Can_Be_DeadLettered_Replayed_And_Sent_After_Onboarding()
    {
        using var dbContext = QaDbContextFactory.CreateContext();
        var services = CreateHarness(dbContext);
        var supplier = CreateAndActivateSupplier(services, "TAX-SUP-NTF-001", "supplier.notify@example.com", "+201000009931");

        var notificationService = new NotificationOrchestrationService(new NotificationOrchestrationRepository(dbContext));
        var queueItem = dbContext.NotificationQueueItems
            .Where(entry => entry.RelatedEntityType == "SUPPLIER" && entry.RelatedEntityId == supplier.PartyId && entry.ChannelCode == "EMAIL")
            .OrderBy(entry => entry.Id)
            .Select(entry => entry.Id)
            .First();

        var deadLettered = notificationService.MoveQueueItemToDeadLetter(queueItem, 4, new NotificationDeadLetterRequest("Simulated provider failure"));
        Assert.Equal("DEAD_LETTER", deadLettered.StatusCode);

        var replayed = notificationService.ReplayQueueItem(queueItem, 4, new NotificationReplayRequest("Retry after provider recovery", 0));
        Assert.Equal("RETRY", replayed.StatusCode);

        var sendResult = notificationService.SendPendingBatch(new NotificationSendBatchRequest("NOTIFICATION_WORKER", 20));
        Assert.Contains(sendResult.SentItems, item => item.NotificationQueueItemId == queueItem && item.StatusCode == "SENT");
    }

    [Fact]
    public void Reporting_Documents_Remain_Drilldown_Consistent_Across_Finance_And_Statements()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var supplierStatementService = new SupplierStatementService(new SupplierStatementRepository(dbContext));
        var customerStatementService = new CustomerStatementService(new CustomerStatementRepository(dbContext));
        var reportingService = new CoreReportingService(new CoreReportingRepository(dbContext));
        var apBillService = new AccountsPayableBillService(new AccountsPayableBillRepository(dbContext));
        var invoiceService = new AccountsReceivableInvoiceService(new AccountsReceivableInvoiceRepository(dbContext));

        var apBill = apBillService.CreateBill(new CreateAccountsPayableBillRequest(
            1,
            1,
            1,
            new DateOnly(2026, 4, 1),
            new DateOnly(2026, 4, 10),
            "EGP",
            [new CreateAccountsPayableBillLineRequest(1, 1001, 2m, "EA", 20m, "VAT14")]));
        apBillService.SubmitBill(apBill.AccountsPayableBillId, 3);
        var apBillEntity = dbContext.AccountsPayableBills.Single(entry => entry.Id == apBill.AccountsPayableBillId);
        apBillEntity.RecordMatchReview(4, true, 0m, "Drilldown reporting approval setup");
        apBillEntity.ApproveForPosting(4);
        dbContext.SaveChanges();
        var postedBill = apBillService.PostBill(apBill.AccountsPayableBillId, 4);

        var postedInvoice = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostInvoice(
            invoiceService,
            invoiceService.CreateInvoice(new CreateAccountsReceivableInvoiceRequest(
                2,
                1,
                1,
                "TAX",
                new DateOnly(2026, 4, 1),
                new DateOnly(2026, 4, 1),
                new DateOnly(2026, 4, 10),
                "EGP",
                [new CreateAccountsReceivableInvoiceLineRequest(1, 1001, 1m, "EA", 30m, "VAT14")])).AccountsReceivableInvoiceId);

        var supplierStatement = supplierStatementService.GenerateStatement(3, new CreateSupplierStatementRequest(
            1,
            new DateOnly(2026, 4, 1),
            new DateOnly(2026, 4, 30),
            "EGP"));
        var customerStatement = customerStatementService.GenerateStatement(3, new CreateCustomerStatementRequest(
            2,
            new DateOnly(2026, 4, 1),
            new DateOnly(2026, 4, 30),
            "EGP"));
        var report = reportingService.GetCoreFinanceReport(new DateOnly(2026, 4, 1), new DateOnly(2026, 4, 30));

        Assert.Contains(supplierStatement.Lines, line => line.DocumentNumber == postedBill.BillNumber);
        Assert.Contains(customerStatement.Lines, line => line.DocumentNumber == postedInvoice.InvoiceNumber);
        Assert.Contains(report.Rows, row => row.DocumentNumber == postedBill.BillNumber);
        Assert.Contains(report.Rows, row => row.DocumentNumber == postedInvoice.InvoiceNumber);
    }

    [Fact]
    public void Supplier_AP_Dispute_Hold_Is_Visible_In_Reconciliation_And_Clears_After_Resolution()
    {
        using var dbContext = QaDbContextFactory.CreateContext();
        var services = CreateHarness(dbContext);
        var supplier = CreateAndActivateSupplier(services, "TAX-SUP-HOLD-001", "supplier.hold@example.com", "+201000009961");

        var apBillService = new AccountsPayableBillService(new AccountsPayableBillRepository(dbContext));
        var reconciliationService = new SupplierAccountReconciliationService(new SupplierAccountReconciliationRepository(dbContext));

        var bill = apBillService.CreateBill(new CreateAccountsPayableBillRequest(
            SupplierId: supplier.PartyId,
            PurchaseOrderId: 1,
            GoodsReceiptId: 1,
            BillDate: new DateOnly(2026, 4, 11),
            DueDate: new DateOnly(2026, 4, 20),
            CurrencyCode: "EGP",
            Lines:
            [
                new CreateAccountsPayableBillLineRequest(1, 1001, 2m, "EA", 20m, "VAT14")
            ]));

        apBillService.SubmitBill(bill.AccountsPayableBillId, 3);
        var heldBill = apBillService.PlaceBillOnHold(bill.AccountsPayableBillId, 4, new PlaceAccountsPayableBillOnHoldRequest(
            ReasonCode: "PRICE_VARIANCE",
            Notes: "Supplier pricing clarification pending."));
        Assert.Equal("ON_HOLD", heldBill.DisputeStatusCode);

        var heldReconciliation = reconciliationService.GetDetail(supplier.PartyId, new DateOnly(2026, 4, 30), "EGP");
        Assert.True(heldReconciliation.OnHoldBillCount >= 1);

        var releasedBill = apBillService.ResolveBillDispute(bill.AccountsPayableBillId, 4, new ResolveAccountsPayableBillDisputeRequest(
            DecisionCode: "RELEASE",
            Notes: "Variance accepted after supplier clarification."));
        Assert.Equal("RESOLVED", releasedBill.DisputeStatusCode);

        var releasedReconciliation = reconciliationService.GetDetail(supplier.PartyId, new DateOnly(2026, 4, 30), "EGP");
        Assert.Equal(0, releasedReconciliation.OnHoldBillCount);
    }

    [Fact]
    public void Settlement_Exceptions_Appear_In_Internal_Exception_Queue_And_Clear_When_Resolved()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var integrationService = new IntegrationEndpointRegistryService(new IntegrationEndpointRegistryRepository(dbContext));
        var paymentChannelService = new PaymentChannelConfigurationService(new PaymentChannelConfigurationRepository(dbContext));
        var refundService = new CustomerRefundService(new CustomerRefundRepository(dbContext), new PaymentGatewayExecutionService(new PaymentGatewayExecutionRepository(dbContext)));
        var receiptService = new AccountsReceivableReceiptService(new AccountsReceivableReceiptRepository(dbContext), refundService);
        var settlementService = new PaymentSettlementBatchService(new PaymentSettlementBatchRepository(dbContext));
        var internalWorkbenchService = new InternalWorkbenchService(new InternalWorkbenchRepository(dbContext));

        var channel = CreateAndActivateRefundChannel(integrationService, paymentChannelService);
        var postedRefund = CreatePostedRefund(receiptService, refundService, channel.PaymentChannelConfigurationId, 140m, "SET-BATCH-CONN-0001");

        var createdBatch = settlementService.CreateBatch(new CreatePaymentSettlementBatchRequest(
            PaymentChannelConfigurationId: channel.PaymentChannelConfigurationId,
            SettlementDate: new DateOnly(2026, 5, 17),
            ExternalBatchReference: "PAYMOB-SETTLE-CONNECTED-001",
            Lines:
            [
                new CreatePaymentSettlementBatchLineRequest(
                    CustomerRefundId: postedRefund.CustomerRefundId,
                    SettledAmount: 135m,
                    FeeAmount: 3m,
                    NetAmount: 132m,
                    ProviderReference: null,
                    Notes: "Connected settlement variance import")
            ]));

        Assert.Equal(2, createdBatch.OpenExceptionCount);

        var exceptions = internalWorkbenchService.GetExceptionQueue("PAYMENT_SETTLEMENT");
        Assert.Contains(exceptions, item => item.SourceType == "PAYMENT_SETTLEMENT" && item.DocumentType == "PAYMENT_SETTLEMENT_EXCEPTION");

        var financeAlerts = internalWorkbenchService.GetOperationalAlerts("HIGH", "FINANCE");
        Assert.Contains(financeAlerts, item => item.AlertCode == "SETTLEMENT_EXCEPTION_OPEN");

        var openExceptions = settlementService.GetExceptions(createdBatch.PaymentSettlementBatchId, "OPEN");
        settlementService.ConfirmException(openExceptions.Single(entry => entry.ExceptionCode == "MISSING_PROVIDER_REFERENCE").PaymentSettlementExceptionId, 4, new ResolvePaymentSettlementExceptionRequest("Provider reference reviewed offline"));
        settlementService.OverrideException(openExceptions.Single(entry => entry.ExceptionCode == "AMOUNT_VARIANCE").PaymentSettlementExceptionId, 4, new ResolvePaymentSettlementExceptionRequest("Provider variance accepted by finance"));

        var exceptionsAfterResolution = internalWorkbenchService.GetExceptionQueue("PAYMENT_SETTLEMENT");
        Assert.DoesNotContain(exceptionsAfterResolution, item => item.SourceType == "PAYMENT_SETTLEMENT" && item.DocumentType == "PAYMENT_SETTLEMENT_EXCEPTION");
    }

    [Fact]
    public void Integration_Recovery_Updates_Dashboard_From_Dead_Letter_And_Retry_To_Clear_State()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var reportingService = new CoreReportingService(new CoreReportingRepository(dbContext));
        var outboxService = new OutboxEventService(new OutboxEventRepository(dbContext));
        var registryService = new IntegrationEndpointRegistryService(new IntegrationEndpointRegistryRepository(dbContext));
        var inboxService = new InboxMessageService(new InboxMessageRepository(dbContext));
        var internalWorkbenchService = new InternalWorkbenchService(new InternalWorkbenchRepository(dbContext));

        reportingService.GenerateReportingSnapshot(2, new CreateReportingSnapshotRequest(
            ReportCode: "CORE_OPERATIONS",
            DateFrom: new DateOnly(2026, 4, 1),
            DateTo: new DateOnly(2026, 4, 30),
            AsOfDate: null,
            Notes: "Connected dead-letter outbox baseline"));

        var outboxEvent = Assert.Single(outboxService.GetOutboxEvents("PENDING", "REPORTING"));
        var deadLetteredOutbox = outboxService.MoveOutboxEventToDeadLetter(outboxEvent.OutboxEventId, 2, new DeadLetterQueueItemRequest(
            ReasonText: "Publisher adapter unavailable"));

        var endpoint = registryService.CreateIntegrationEndpoint(new CreateIntegrationEndpointRequest(
            EndpointName: "Connected recovery inbox endpoint",
            IntegrationProviderCode: "NOTIFICATION",
            EnvironmentCode: "TEST",
            DirectionCode: "INBOUND",
            TransportCode: "WEBHOOK",
            HttpMethodCode: "POST",
            BaseUrl: "https://notify.test.example",
            RelativePath: "/callbacks/connected-recovery",
            AuthenticationModeCode: "BEARER",
            CredentialReference: "secret://notify/test/bearer",
            TimeoutSeconds: 20,
            RetryPolicyCode: "STANDARD",
            MaxRetryCount: 2,
            IsIdempotencyKeyRequired: true,
            Notes: "Connected recovery endpoint"));
        registryService.SubmitIntegrationEndpoint(endpoint.IntegrationEndpointId, 2);
        registryService.ApproveIntegrationEndpoint(endpoint.IntegrationEndpointId, 4);
        registryService.ActivateIntegrationEndpoint(endpoint.IntegrationEndpointId, 4);

        var inboxMessage = inboxService.ReceiveInboxMessage(2, new ReceiveInboxMessageRequest(
            IntegrationEndpointId: endpoint.IntegrationEndpointId,
            MessageType: "STATUS_UPDATE",
            ExternalMessageId: "NTF-CONN-0001",
            CorrelationKey: "ORDER-CONN-001",
            PayloadText: "{\"status\":\"queued\"}"));
        var deadLetteredInbox = inboxService.MoveInboxMessageToDeadLetter(inboxMessage.InboxMessageId, 2, new DeadLetterQueueItemRequest(
            ReasonText: "Inbound handler failed"));

        var degradedDashboard = internalWorkbenchService.GetOperationalDashboard("Testing", "1.0.0.0");
        Assert.True(degradedDashboard.DeadLetterOutboxCount >= 1);
        Assert.True(degradedDashboard.DeadLetterInboxCount >= 1);

        outboxService.ReplayOutboxEvent(deadLetteredOutbox.OutboxEventId, 2, new ReplayDeadLetterQueueItemRequest(
            ReplayReasonText: "Retry after transport recovery",
            DelaySeconds: 0));
        inboxService.ReplayInboxMessage(deadLetteredInbox.InboxMessageId, 2, new ReplayDeadLetterQueueItemRequest(
            ReplayReasonText: "Retry after handler fix",
            DelaySeconds: 0));

        var retryDashboard = internalWorkbenchService.GetOperationalDashboard("Testing", "1.0.0.0");
        Assert.True(retryDashboard.RetryInboxCount >= 1);

        var integrationAlerts = internalWorkbenchService.GetOperationalAlerts(sourceArea: "INTEGRATION");
        Assert.Contains(integrationAlerts, item => item.AlertCode == "INBOX_RETRY_BACKLOG");

        outboxService.PublishPendingEvents(new PublishOutboxBatchRequest(
            PublisherCode: "CONNECTED_RECOVERY_WORKER",
            BatchSize: 10));
        inboxService.ProcessPendingMessages(new ProcessInboxBatchRequest(
            ProcessorCode: "CONNECTED_RECOVERY_INBOX_WORKER",
            BatchSize: 10));

        var recoveredDashboard = internalWorkbenchService.GetOperationalDashboard("Testing", "1.0.0.0");
        Assert.Equal(0, recoveredDashboard.DeadLetterOutboxCount);
        Assert.Equal(0, recoveredDashboard.DeadLetterInboxCount);
        Assert.Equal(0, recoveredDashboard.RetryInboxCount);
    }

    [Fact]
    public void Supplier_Purchase_Return_And_Debit_Note_Remain_Connected_To_Finance_Report_And_Reconciliation()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var purchaseReturnService = new PurchaseReturnService(new PurchaseReturnRepository(dbContext));
        var accountsPayableBillService = new AccountsPayableBillService(new AccountsPayableBillRepository(dbContext));
        var reconciliationService = new SupplierAccountReconciliationService(new SupplierAccountReconciliationRepository(dbContext));
        var reportingService = new CoreReportingService(new CoreReportingRepository(dbContext));

        var createdReturn = purchaseReturnService.CreatePurchaseReturn(new CreatePurchaseReturnRequest(
            BranchId: 1,
            SupplierId: 1,
            PurchaseOrderId: 1,
            GoodsReceiptId: 1,
            ReturnDate: new DateOnly(2026, 5, 7),
            ReturnWarehouseId: 1,
            ReasonCode: "SUPPLIER_REJECTION",
            Notes: "Connected supplier return.",
            Lines:
            [
                new CreatePurchaseReturnLineRequest(1, 1, 1001, 3m, null, "BATCH-1001-A", null, "Connected returned units")
            ]));

        purchaseReturnService.SubmitPurchaseReturn(createdReturn.PurchaseReturnId, 3);
        var postedReturn = purchaseReturnService.PostPurchaseReturn(createdReturn.PurchaseReturnId, 4);

        var issuedDebitNote = purchaseReturnService.IssueDebitNote(createdReturn.PurchaseReturnId, new IssuePurchaseReturnDebitNoteRequest(
            ActorUserId: 4,
            BillDate: new DateOnly(2026, 5, 7),
            DueDate: new DateOnly(2026, 5, 20)));
        var debitNote = accountsPayableBillService.GetBill(issuedDebitNote.DebitNoteBillId!.Value)!;

        accountsPayableBillService.SubmitBill(debitNote.AccountsPayableBillId, 5);
        accountsPayableBillService.ApproveMatch(debitNote.AccountsPayableBillId, 6);
        var postedDebitNote = accountsPayableBillService.PostBill(debitNote.AccountsPayableBillId, 6);

        var reconciliation = reconciliationService.GetDetail(1, new DateOnly(2026, 5, 31), "EGP");
        var financeReport = reportingService.GetCoreFinanceReport(new DateOnly(2026, 5, 1), new DateOnly(2026, 5, 31));
        var operationsReport = reportingService.GetCoreOperationsReport(new DateOnly(2026, 5, 1), new DateOnly(2026, 5, 31));

        Assert.Contains(reconciliation.Lines, line => line.DocumentNumber == postedDebitNote.BillNumber);
        Assert.Contains(financeReport.Rows, row => row.DocumentNumber == postedDebitNote.BillNumber);
        Assert.Contains(operationsReport.Rows, row => row.DocumentNumber == postedReturn.PurchaseReturnNumber && row.ActivityType == "PURCHASE_RETURN");
    }

    [Fact]
    public void Customer_Sales_Return_And_Credit_Note_Remain_Connected_To_Statement_And_Reports()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var salesOrderService = new SalesOrderService(new SalesOrderRepository(dbContext));
        var dispatchService = new DispatchOrderService(new DispatchOrderRepository(dbContext));
        var salesReturnService = new SalesReturnService(new SalesReturnRepository(dbContext));
        var invoiceService = new AccountsReceivableInvoiceService(new AccountsReceivableInvoiceRepository(dbContext));
        var statementService = new CustomerStatementService(new CustomerStatementRepository(dbContext));
        var reportingService = new CoreReportingService(new CoreReportingRepository(dbContext));

        var salesOrder = salesOrderService.CreateSalesOrder(new CreateSalesOrderRequest(
            BranchId: 1,
            CustomerId: 2,
            OrderDate: new DateOnly(2026, 4, 29),
            CurrencyCode: "EGP",
            ExchangeRate: 1m,
            Lines:
            [
                new CreateSalesOrderLineRequest(1001, 1, 4m, "EA", 32m, "VAT14")
            ]));
        var submittedSalesOrder = salesOrderService.SubmitSalesOrder(salesOrder.SalesOrderId, 2);

        var dispatch = dispatchService.CreateDispatchOrder(new CreateDispatchOrderRequest(
            BranchId: 1,
            SalesOrderId: submittedSalesOrder.SalesOrderId,
            DispatchWarehouseId: 1,
            PlannedShipAtUtc: new DateTime(2026, 4, 29, 10, 0, 0, DateTimeKind.Utc),
            Lines:
            [
                new CreateDispatchOrderLineRequest(submittedSalesOrder.Lines.First().SalesOrderLineId, 1001, 4m, null, "BATCH-RET-002", null)
            ]));
        var postedDispatch = dispatchService.PostDispatchOrder(dispatch.DispatchOrderId, new PostDispatchOrderRequest(
            ActorUserId: 6,
            ShippedAtUtc: new DateTime(2026, 4, 29, 12, 0, 0, DateTimeKind.Utc),
            DeliveredAtUtc: new DateTime(2026, 4, 29, 15, 0, 0, DateTimeKind.Utc),
            ProofOfDeliveryReceiver: "Connected return receiver"));

        var salesReturn = salesReturnService.CreateSalesReturn(new CreateSalesReturnRequest(
            BranchId: 1,
            CustomerId: 2,
            SalesOrderId: submittedSalesOrder.SalesOrderId,
            DispatchOrderId: postedDispatch.DispatchOrderId,
            ReturnDate: new DateOnly(2026, 4, 30),
            ReturnWarehouseId: 1,
            ReasonCode: "CUSTOMER_RETURN",
            Notes: "Connected customer return.",
            Lines:
            [
                new CreateSalesReturnLineRequest(postedDispatch.Lines.First().DispatchOrderLineId, submittedSalesOrder.Lines.First().SalesOrderLineId, 1001, 2m, null, "BATCH-RET-002", null, "2 units returned")
            ]));

        salesReturnService.SubmitSalesReturn(salesReturn.SalesReturnId, 3);
        var postedReturn = salesReturnService.PostSalesReturn(salesReturn.SalesReturnId, 4);
        var returnWithCreditNote = salesReturnService.IssueCreditNote(
            postedReturn.SalesReturnId,
            new IssueSalesReturnCreditNoteRequest(
                ActorUserId: 4,
                InvoiceDate: new DateOnly(2026, 4, 30),
                TaxDate: new DateOnly(2026, 4, 30),
                DueDate: new DateOnly(2026, 4, 30)));

        var postedCreditNote = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostInvoice(
            invoiceService,
            returnWithCreditNote.CreditNoteInvoiceId!.Value);

        var statement = statementService.GenerateStatement(3, new CreateCustomerStatementRequest(
            CustomerId: 2,
            DateFrom: new DateOnly(2026, 4, 1),
            DateTo: new DateOnly(2026, 4, 30),
            CurrencyCode: "EGP"));
        var financeReport = reportingService.GetCoreFinanceReport(new DateOnly(2026, 4, 1), new DateOnly(2026, 4, 30));
        var operationsReport = reportingService.GetCoreOperationsReport(new DateOnly(2026, 4, 1), new DateOnly(2026, 4, 30));

        Assert.Contains(statement.Lines, line => line.DocumentNumber == postedCreditNote.InvoiceNumber);
        Assert.Contains(financeReport.Rows, row => row.DocumentNumber == postedCreditNote.InvoiceNumber);
        Assert.Contains(operationsReport.Rows, row => row.DocumentNumber == postedReturn.SalesReturnNumber && row.ActivityType == "SALES_RETURN");
    }

    [Fact]
    public void Notification_Dead_Letter_Alert_Clears_After_Replay_And_Send()
    {
        using var dbContext = QaDbContextFactory.CreateContext();
        var services = CreateHarness(dbContext);
        var supplier = CreateAndActivateSupplier(services, "TAX-SUP-NTF-ALERT-001", "supplier.ntf.alert@example.com", "+201000009962");

        var notificationService = new NotificationOrchestrationService(new NotificationOrchestrationRepository(dbContext));
        var internalWorkbenchService = new InternalWorkbenchService(new InternalWorkbenchRepository(dbContext));

        var queueItem = dbContext.NotificationQueueItems
            .Where(entry => entry.RelatedEntityType == "SUPPLIER" && entry.RelatedEntityId == supplier.PartyId && entry.ChannelCode == "EMAIL")
            .OrderByDescending(entry => entry.Id)
            .Select(entry => entry.Id)
            .First();

        notificationService.MoveQueueItemToDeadLetter(queueItem, 4, new NotificationDeadLetterRequest("SMTP unavailable"));

        var notificationAlerts = internalWorkbenchService.GetOperationalAlerts("HIGH", "NOTIFICATIONS");
        Assert.Contains(notificationAlerts, item => item.AlertCode == "NOTIFICATION_DEAD_LETTER");

        notificationService.ReplayQueueItem(queueItem, 4, new NotificationReplayRequest("Mail service restored", 0));
        notificationService.SendPendingBatch(new NotificationSendBatchRequest("NOTIFICATION_WORKER", 20));

        var notificationAlertsAfterRecovery = internalWorkbenchService.GetOperationalAlerts("HIGH", "NOTIFICATIONS");
        Assert.DoesNotContain(notificationAlertsAfterRecovery, item => item.AlertCode == "NOTIFICATION_DEAD_LETTER");
    }

    [Fact]
    public void Dashboard_And_Report_Views_Stay_Drilldown_Consistent_For_Settlement_And_Recovery_Artifacts()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var reportingService = new CoreReportingService(new CoreReportingRepository(dbContext));
        var outboxService = new OutboxEventService(new OutboxEventRepository(dbContext));
        var registryService = new IntegrationEndpointRegistryService(new IntegrationEndpointRegistryRepository(dbContext));
        var inboxService = new InboxMessageService(new InboxMessageRepository(dbContext));
        var internalWorkbenchService = new InternalWorkbenchService(new InternalWorkbenchRepository(dbContext));

        reportingService.GenerateReportingSnapshot(2, new CreateReportingSnapshotRequest(
            ReportCode: "CORE_FINANCE",
            DateFrom: new DateOnly(2026, 4, 1),
            DateTo: new DateOnly(2026, 4, 30),
            AsOfDate: null,
            Notes: "Dashboard report drilldown consistency"));

        var outboxEvent = Assert.Single(outboxService.GetOutboxEvents("PENDING", "REPORTING"));
        outboxService.MoveOutboxEventToDeadLetter(outboxEvent.OutboxEventId, 2, new DeadLetterQueueItemRequest("Publisher unavailable"));

        var endpoint = CreateAndActivateEndpoint(registryService, "Drilldown inbox endpoint", "NOTIFICATION", "INBOUND", "/callbacks/drilldown");
        var inboxMessage = inboxService.ReceiveInboxMessage(2, new ReceiveInboxMessageRequest(
            IntegrationEndpointId: endpoint.IntegrationEndpointId,
            MessageType: "STATUS_UPDATE",
            ExternalMessageId: "NTF-DRILL-0001",
            CorrelationKey: "ORDER-DRILL-001",
            PayloadText: "{\"status\":\"retry\"}"));
        inboxService.MoveInboxMessageToDeadLetter(inboxMessage.InboxMessageId, 2, new DeadLetterQueueItemRequest("Handler unavailable"));

        var dashboard = internalWorkbenchService.GetOperationalDashboard("Testing", "1.0.0.0");
        var alerts = internalWorkbenchService.GetOperationalAlerts(sourceArea: "INTEGRATION");
        Assert.True(dashboard.DeadLetterOutboxCount >= 1);
        Assert.True(dashboard.DeadLetterInboxCount >= 1);
        Assert.Contains(alerts, item => item.AlertCode == "OUTBOX_DEAD_LETTER");
        Assert.Contains(alerts, item => item.AlertCode == "INBOX_DEAD_LETTER");
    }

    [Fact]
    public void Eta_Rejection_Resubmission_And_Acceptance_Remain_Synchronized_Across_Logs_Events_And_Internal_Exception_Visibility()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var invoiceService = new AccountsReceivableInvoiceService(new AccountsReceivableInvoiceRepository(dbContext));
        var etaService = new EtaComplianceService(new EtaComplianceRepository(dbContext));
        var internalWorkbenchService = new InternalWorkbenchService(new InternalWorkbenchRepository(dbContext));

        var postedInvoice = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostInvoice(
            invoiceService,
            invoiceService.CreateInvoice(new CreateAccountsReceivableInvoiceRequest(
                CustomerId: 2,
                SalesOrderId: 1,
                DispatchOrderId: 1,
                InvoiceType: "TAX",
                InvoiceDate: new DateOnly(2026, 5, 21),
                TaxDate: new DateOnly(2026, 5, 21),
                DueDate: new DateOnly(2026, 5, 31),
                CurrencyCode: "EGP",
                Lines:
                [
                    new CreateAccountsReceivableInvoiceLineRequest(1, 1001, 2m, "EA", 32m, "VAT14")
                ])).AccountsReceivableInvoiceId);

        var initialSubmission = etaService.SubmitInvoice(postedInvoice.AccountsReceivableInvoiceId, new SubmitInvoiceToEtaRequest(
            ActorUserId: 4,
            PayloadText: null));
        etaService.RecordInvoiceResponse(
            postedInvoice.AccountsReceivableInvoiceId,
            initialSubmission.EtaSubmissionLogId,
            new RecordEtaInvoiceResponseRequest(
                ActorUserId: 4,
                StatusCode: "REJECTED",
                EtaUuid: null,
                ResponseText: "{\"status\":\"rejected\"}",
                ErrorCode: "ETA-422",
                ErrorMessage: "Buyer branch details require correction.",
                NotesText: "ETA rejected the first submission."));

        var rejectedExceptions = internalWorkbenchService.GetExceptionQueue("ETA");
        Assert.Contains(rejectedExceptions, item => item.DocumentId == postedInvoice.AccountsReceivableInvoiceId && item.ExceptionCategory == "REJECTED");

        var resubmitted = etaService.ResubmitRejectedInvoice(
            postedInvoice.AccountsReceivableInvoiceId,
            initialSubmission.EtaSubmissionLogId,
            new ResubmitEtaInvoiceRequest(
                ActorUserId: 4,
                PayloadText: null,
                CorrectionNotes: "Branch metadata corrected before resubmission."));

        etaService.RecordInvoiceResponse(
            postedInvoice.AccountsReceivableInvoiceId,
            resubmitted.EtaSubmissionLogId,
            new RecordEtaInvoiceResponseRequest(
                ActorUserId: 4,
                StatusCode: "ACCEPTED",
                EtaUuid: "ETA-CONNECTED-0001",
                ResponseText: "{\"status\":\"accepted\"}",
                ErrorCode: null,
                ErrorMessage: null,
                NotesText: "ETA accepted the corrected submission."));

        var logs = etaService.GetSubmissionLogs(postedInvoice.AccountsReceivableInvoiceId);
        var events = etaService.GetDocumentEvents(postedInvoice.AccountsReceivableInvoiceId);
        var remainingExceptions = internalWorkbenchService.GetExceptionQueue("ETA");

        Assert.Equal(2, logs.Count);
        Assert.Contains(logs, item => item.EtaSubmissionLogId == initialSubmission.EtaSubmissionLogId && item.StatusCode == "REJECTED");
        Assert.Contains(logs, item => item.EtaSubmissionLogId == resubmitted.EtaSubmissionLogId && item.StatusCode == "ACCEPTED");
        Assert.Contains(events, item => item.EventType == "CORRECTION" && item.EventStatus == "PREPARED");
        Assert.Contains(events, item => item.EventType == "RESUBMISSION" && item.EventStatus == "SUBMITTED");
        Assert.Contains(events, item => item.EventType == "RESPONSE" && item.EventStatus == "ACCEPTED");
        Assert.DoesNotContain(remainingExceptions, item => item.DocumentId == postedInvoice.AccountsReceivableInvoiceId);
    }

    [Fact]
    public void Supplier_Procurement_To_AP_Remains_Consistent_Between_Supplier_Portal_Internal_Reconciliation_And_Reporting()
    {
        using var dbContext = QaDbContextFactory.CreateContext();
        var services = CreateHarness(dbContext);
        var supplier = CreateAndActivateSupplier(services, "TAX-SUP-PORTAL-AP-001", "supplier.portal.ap@example.com", "+201000009971");

        var externalPortalService = CreateExternalPortalService(dbContext);
        var purchaseRequisitionService = new PurchaseRequisitionService(new PurchaseRequisitionRepository(dbContext));
        var rfqService = new RfqService(new RfqRepository(dbContext));
        var quotationService = new SupplierQuotationService(new SupplierQuotationRepository(dbContext));
        var evaluationService = new RfqEvaluationService(new RfqEvaluationRepository(dbContext));
        var goodsReceiptService = new GoodsReceiptService(new GoodsReceiptRepository(dbContext));
        var apBillService = new AccountsPayableBillService(new AccountsPayableBillRepository(dbContext));
        var apPaymentService = new AccountsPayablePaymentService(new AccountsPayablePaymentRepository(dbContext));
        var statementService = new SupplierStatementService(new SupplierStatementRepository(dbContext));
        var reconciliationService = new SupplierAccountReconciliationService(new SupplierAccountReconciliationRepository(dbContext));
        var reportingService = new CoreReportingService(new CoreReportingRepository(dbContext));

        var requisition = purchaseRequisitionService.CreatePurchaseRequisition(new CreatePurchaseRequisitionRequest(
            BranchId: 1,
            RequesterUserId: 1,
            RequisitionDate: new DateOnly(2026, 5, 22),
            NeedByDate: new DateOnly(2026, 5, 24),
            PurposeText: "Portal AP continuity flow",
            CurrencyCode: "EGP",
            Lines: [new PurchaseRequisitionLineRequest(1001, 4m, "EA", new DateOnly(2026, 5, 24), 28m, "Portal AP item")]));
        var submittedRequisition = purchaseRequisitionService.SubmitPurchaseRequisition(requisition.PurchaseRequisitionId, 1);

        var rfq = rfqService.CreateRfq(new CreateRfqRequest(
            BranchId: 1,
            DepartmentId: 3,
            PurchaseRequisitionId: submittedRequisition.PurchaseRequisitionId,
            RequesterUserId: 1,
            BuyerUserId: 2,
            RfqType: "STANDARD",
            RfqDate: new DateOnly(2026, 5, 22),
            ValidUntilDate: new DateOnly(2026, 5, 23),
            CurrencyCode: "EGP",
            Notes: "Portal AP RFQ",
            Lines:
            [
                new CreateRfqLineRequest(
                    submittedRequisition.Lines.First().LineId,
                    submittedRequisition.Lines.First().ItemId,
                    "Portal AP item",
                    4m,
                    "EA",
                    new DateOnly(2026, 5, 24),
                    "VAT14",
                    28m,
                    null)
            ],
            SupplierIds: [supplier.SupplierProfileId]));
        var submittedRfq = rfqService.SubmitRfq(rfq.RfqId, 2);

        var quotation = quotationService.CreateQuotation(new CreateSupplierQuotationRequest(
            RequestForQuotationId: submittedRfq.RfqId,
            SupplierId: supplier.SupplierProfileId,
            QuoteDate: new DateOnly(2026, 5, 22),
            ValidUntilDate: new DateOnly(2026, 5, 26),
            CurrencyCode: "EGP",
            FxRate: 1m,
            Notes: "Portal AP quote",
            Lines:
            [
                new CreateSupplierQuotationLineRequest(
                    submittedRfq.Lines.First().RfqLineId,
                    submittedRfq.Lines.First().ItemId,
                    4m,
                    "EA",
                    28m,
                    0m,
                    0m,
                    "VAT14",
                    2)
            ]));
        var submittedQuotation = quotationService.SubmitQuotation(quotation.SupplierQuotationId, 2);

        var evaluation = evaluationService.CreateEvaluation(new CreateRfqEvaluationRequest(
            RequestForQuotationId: submittedRfq.RfqId,
            EvaluationDate: new DateOnly(2026, 5, 22),
            Notes: "Portal AP evaluation",
            Lines:
            [
                new CreateRfqEvaluationLineRequest(
                    submittedQuotation.SupplierQuotationId,
                    submittedQuotation.SupplierId,
                    95m,
                    94m,
                    93m,
                    96m,
                    1,
                    true,
                    "Recommended")
            ]));
        evaluationService.FinalizeRecommendation(evaluation.RfqEvaluationId, 3);
        var purchaseOrder = evaluationService.AwardPurchaseOrder(evaluation.RfqEvaluationId, new AwardPurchaseOrderRequest(4, new DateOnly(2026, 5, 22), 1));

        var goodsReceipt = goodsReceiptService.PostGoodsReceipt(
            goodsReceiptService.CreateGoodsReceipt(new CreateGoodsReceiptRequest(
                BranchId: 1,
                PurchaseOrderId: purchaseOrder.PurchaseOrderId,
                SupplierId: purchaseOrder.SupplierId,
                ReceivedAtUtc: new DateTime(2026, 5, 23, 9, 0, 0, DateTimeKind.Utc),
                ReceivedByUserId: 4,
                InspectionStatus: "ACCEPTED",
                Lines:
                [
                    new CreateGoodsReceiptLineRequest(
                        purchaseOrder.Lines.First().PurchaseOrderLineId,
                        purchaseOrder.Lines.First().ItemId,
                        purchaseOrder.Lines.First().WarehouseId,
                        null,
                        4m,
                        4m,
                        0m,
                        "SUP-PORTAL-AP-GRN-1",
                        null)
                ])).GoodsReceiptId,
            4);

        var bill = apBillService.CreateBill(new CreateAccountsPayableBillRequest(
            SupplierId: supplier.PartyId,
            PurchaseOrderId: purchaseOrder.PurchaseOrderId,
            GoodsReceiptId: goodsReceipt.GoodsReceiptId,
            BillDate: new DateOnly(2026, 5, 23),
            DueDate: new DateOnly(2026, 5, 30),
            CurrencyCode: "EGP",
            Lines:
            [
                new CreateAccountsPayableBillLineRequest(
                    purchaseOrder.Lines.First().PurchaseOrderLineId,
                    purchaseOrder.Lines.First().ItemId,
                    4m,
                    "EA",
                    28m,
                    "VAT14")
            ]));
        apBillService.SubmitBill(bill.AccountsPayableBillId, 3);
        apBillService.ApproveMatch(bill.AccountsPayableBillId, 4);
        var postedBill = apBillService.PostBill(bill.AccountsPayableBillId, 4);

        var payment = apPaymentService.CreatePayment(new CreateAccountsPayablePaymentRequest(
            SupplierId: supplier.PartyId,
            PaymentDate: new DateOnly(2026, 5, 24),
            PaymentMethod: "BANK_TRANSFER",
            CurrencyCode: "EGP",
            AmountAmount: 70m,
            ReferenceNumber: "SUP-PORTAL-AP-PAY-001",
            Notes: "Portal AP settlement",
            Allocations:
            [
                new CreateAccountsPayablePaymentAllocationRequest(postedBill.AccountsPayableBillId, 70m, new DateOnly(2026, 5, 24))
            ]));
        apPaymentService.SubmitPayment(payment.AccountsPayablePaymentId, 5);
        apPaymentService.ApprovePayment(payment.AccountsPayablePaymentId, 6);
        var postedPayment = apPaymentService.PostPayment(payment.AccountsPayablePaymentId, 6);

        var supplierStatement = statementService.GenerateStatement(3, new CreateSupplierStatementRequest(
            supplier.PartyId,
            new DateOnly(2026, 5, 1),
            new DateOnly(2026, 5, 31),
            "EGP"));
        var portalInvoices = externalPortalService.GetInvoices(supplier.PartyId);
        var portalPayments = externalPortalService.GetPayments(supplier.PartyId);
        var portalSummary = externalPortalService.GetStatementSummary(supplier.PartyId, new DateOnly(2026, 5, 1), new DateOnly(2026, 5, 31), "EGP");
        var portalLines = externalPortalService.GetStatementLines(supplier.PartyId, new DateOnly(2026, 5, 1), new DateOnly(2026, 5, 31), "EGP");
        var reconciliation = reconciliationService.GetDetail(supplier.PartyId, new DateOnly(2026, 5, 31), "EGP");
        var report = reportingService.GetCoreFinanceReport(new DateOnly(2026, 5, 1), new DateOnly(2026, 5, 31));

        Assert.Contains(supplierStatement.Lines, line => line.DocumentNumber == postedBill.BillNumber);
        Assert.Contains(portalInvoices, item => item.InvoiceNumber == postedBill.BillNumber);
        Assert.Contains(portalPayments, item => item.PaymentNumber == postedPayment.PaymentNumber);
        Assert.Contains(portalLines, item => item.DocumentNumber == postedBill.BillNumber);
        Assert.Contains(portalLines, item => item.DocumentNumber == postedPayment.PaymentNumber);
        Assert.True(portalSummary.InvoiceCount >= 1);
        Assert.True(portalSummary.PaymentCount >= 1);
        Assert.Contains(reconciliation.Lines, line => line.DocumentNumber == postedBill.BillNumber);
        Assert.Contains(reconciliation.Lines, line => line.DocumentNumber == postedPayment.PaymentNumber);
        Assert.Contains(report.Rows, row => row.DocumentNumber == postedBill.BillNumber);
        Assert.Contains(report.Rows, row => row.DocumentNumber == postedPayment.PaymentNumber);
    }

    [Fact]
    public void Customer_Sales_To_AR_Remains_Consistent_Between_Customer_Portal_Internal_Reporting_And_Statement_Read_Models()
    {
        using var dbContext = QaDbContextFactory.CreateContext();
        var services = CreateHarness(dbContext);
        var customer = CreateAndActivateCustomer(services, "TAX-CUS-PORTAL-AR-001", "customer.portal.ar@example.com", "+201000009972");

        var externalPortalService = CreateExternalPortalService(dbContext);
        var salesOrderService = new SalesOrderService(new SalesOrderRepository(dbContext));
        var dispatchService = new DispatchOrderService(new DispatchOrderRepository(dbContext));
        var invoiceService = new AccountsReceivableInvoiceService(new AccountsReceivableInvoiceRepository(dbContext));
        var receiptService = new AccountsReceivableReceiptService(new AccountsReceivableReceiptRepository(dbContext));
        var customerStatementService = new CustomerStatementService(new CustomerStatementRepository(dbContext));
        var reportingService = new CoreReportingService(new CoreReportingRepository(dbContext));

        var salesOrder = salesOrderService.CreateSalesOrder(new CreateSalesOrderRequest(
            BranchId: 1,
            CustomerId: customer.PartyId,
            OrderDate: new DateOnly(2026, 5, 22),
            CurrencyCode: "EGP",
            ExchangeRate: 1m,
            Lines:
            [
                new CreateSalesOrderLineRequest(1001, 1, 5m, "EA", 45m, "VAT14")
            ]));
        var submittedSalesOrder = salesOrderService.SubmitSalesOrder(salesOrder.SalesOrderId, 2);

        var dispatch = dispatchService.PostDispatchOrder(
            dispatchService.CreateDispatchOrder(new CreateDispatchOrderRequest(
                BranchId: 1,
                SalesOrderId: submittedSalesOrder.SalesOrderId,
                DispatchWarehouseId: 1,
                PlannedShipAtUtc: new DateTime(2026, 5, 22, 11, 0, 0, DateTimeKind.Utc),
                Lines:
                [
                    new CreateDispatchOrderLineRequest(
                        submittedSalesOrder.Lines.First().SalesOrderLineId,
                        submittedSalesOrder.Lines.First().ItemId,
                        5m,
                        null,
                        "CUS-PORTAL-AR-DSP-1",
                        null)
                ])).DispatchOrderId,
            new PostDispatchOrderRequest(
                6,
                new DateTime(2026, 5, 22, 13, 0, 0, DateTimeKind.Utc),
                new DateTime(2026, 5, 22, 16, 0, 0, DateTimeKind.Utc),
                "Portal AR receiver"));

        var postedInvoice = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostInvoice(
            invoiceService,
            invoiceService.CreateInvoice(new CreateAccountsReceivableInvoiceRequest(
                CustomerId: customer.PartyId,
                SalesOrderId: submittedSalesOrder.SalesOrderId,
                DispatchOrderId: dispatch.DispatchOrderId,
                InvoiceType: "TAX",
                InvoiceDate: new DateOnly(2026, 5, 22),
                TaxDate: new DateOnly(2026, 5, 22),
                DueDate: new DateOnly(2026, 6, 1),
                CurrencyCode: "EGP",
                Lines:
                [
                    new CreateAccountsReceivableInvoiceLineRequest(
                        submittedSalesOrder.Lines.First().SalesOrderLineId,
                        submittedSalesOrder.Lines.First().ItemId,
                        5m,
                        "EA",
                        45m,
                        "VAT14")
                ])).AccountsReceivableInvoiceId);

        var postedReceipt = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostReceipt(
            receiptService,
            receiptService.CreateReceipt(new CreateAccountsReceivableReceiptRequest(
                CustomerId: customer.PartyId,
                ReceiptDate: new DateOnly(2026, 5, 24),
                ReceiptMethod: "BANK_TRANSFER",
                CurrencyCode: "EGP",
                AmountAmount: 100m,
                ReferenceNumber: "CUS-PORTAL-AR-RCPT-001",
                Notes: "Portal AR receipt",
                Allocations:
                [
                    new CreateAccountsReceivableReceiptAllocationRequest(postedInvoice.AccountsReceivableInvoiceId, 100m, new DateOnly(2026, 5, 24))
                ])).AccountsReceivableReceiptId);

        var statement = customerStatementService.GenerateStatement(3, new CreateCustomerStatementRequest(
            customer.PartyId,
            new DateOnly(2026, 5, 1),
            new DateOnly(2026, 5, 31),
            "EGP"));
        var portalOrders = externalPortalService.GetOrders(customer.PartyId);
        var portalInvoices = externalPortalService.GetInvoices(customer.PartyId);
        var portalPayments = externalPortalService.GetPayments(customer.PartyId);
        var portalDeliveries = externalPortalService.GetCustomerDeliveries(customer.PartyId);
        var portalSummary = externalPortalService.GetStatementSummary(customer.PartyId, new DateOnly(2026, 5, 1), new DateOnly(2026, 5, 31), "EGP");
        var portalLines = externalPortalService.GetStatementLines(customer.PartyId, new DateOnly(2026, 5, 1), new DateOnly(2026, 5, 31), "EGP");
        var report = reportingService.GetCoreFinanceReport(new DateOnly(2026, 5, 1), new DateOnly(2026, 5, 31));

        Assert.Contains(statement.Lines, line => line.DocumentNumber == postedInvoice.InvoiceNumber);
        Assert.Contains(statement.Lines, line => line.DocumentNumber == postedReceipt.ReceiptNumber);
        Assert.Contains(portalOrders, item => item.OrderId == submittedSalesOrder.SalesOrderId);
        Assert.Contains(portalInvoices, item => item.InvoiceNumber == postedInvoice.InvoiceNumber);
        Assert.Contains(portalPayments, item => item.PaymentNumber == postedReceipt.ReceiptNumber);
        Assert.Contains(portalDeliveries, item => item.DispatchOrderId == dispatch.DispatchOrderId);
        Assert.Contains(portalLines, item => item.DocumentNumber == postedInvoice.InvoiceNumber);
        Assert.Contains(portalLines, item => item.DocumentNumber == postedReceipt.ReceiptNumber);
        Assert.True(portalSummary.OrderCount >= 1);
        Assert.True(portalSummary.InvoiceCount >= 1);
        Assert.True(portalSummary.PaymentCount >= 1);
        Assert.Contains(report.Rows, row => row.DocumentNumber == postedInvoice.InvoiceNumber);
        Assert.Contains(report.Rows, row => row.DocumentNumber == postedReceipt.ReceiptNumber);
    }

    [Fact]
    public void Customer_Receipt_Reversal_Reallocation_And_Refund_Remain_Consistent_Between_Customer_Portal_Statements_And_Refund_Views()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var externalPortalService = CreateExternalPortalService(dbContext);
        var refundService = new CustomerRefundService(new CustomerRefundRepository(dbContext), new PaymentGatewayExecutionService(new PaymentGatewayExecutionRepository(dbContext)));
        var receiptService = new AccountsReceivableReceiptService(new AccountsReceivableReceiptRepository(dbContext), refundService);
        var customerStatementService = new CustomerStatementService(new CustomerStatementRepository(dbContext));

        var originalReceipt = receiptService.CreateReceipt(new CreateAccountsReceivableReceiptRequest(
            CustomerId: 2,
            ReceiptDate: new DateOnly(2026, 5, 25),
            ReceiptMethod: "BANK_TRANSFER",
            CurrencyCode: "EGP",
            AmountAmount: 260m,
            ReferenceNumber: "CUS-REVERSE-0001",
            Notes: "Original receipt before reversal",
            Allocations: []));
        receiptService.SubmitReceipt(originalReceipt.AccountsReceivableReceiptId, 3);
        receiptService.ApproveReceipt(originalReceipt.AccountsReceivableReceiptId, 4);
        var postedOriginalReceipt = receiptService.PostReceipt(originalReceipt.AccountsReceivableReceiptId, 4);

        var reversedReceipt = receiptService.ReverseReceipt(postedOriginalReceipt.AccountsReceivableReceiptId, 4, new ReverseAccountsReceivableReceiptRequest(
            ReversalDate: new DateOnly(2026, 5, 26),
            ReasonCode: "MISAPPLIED",
            Notes: "Original receipt reversed before reallocation."));
        Assert.Equal("REVERSED", reversedReceipt.StatusCode);

        var replacementReceipt = receiptService.ReallocateReceipt(postedOriginalReceipt.AccountsReceivableReceiptId, 4, new ReallocateAccountsReceivableReceiptRequest(
            ReceiptDate: new DateOnly(2026, 5, 26),
            ReceiptMethod: "BANK_TRANSFER",
            ReferenceNumber: "CUS-REVERSE-0001-R1",
            Notes: "Replacement receipt after reversal.",
            Allocations: []));
        receiptService.SubmitReceipt(replacementReceipt.AccountsReceivableReceiptId, 3);
        receiptService.ApproveReceipt(replacementReceipt.AccountsReceivableReceiptId, 4);
        var postedReplacementReceipt = receiptService.PostReceipt(replacementReceipt.AccountsReceivableReceiptId, 4);

        var createdRefund = refundService.CreateRefund(new CreateCustomerRefundRequest(
            CustomerId: 2,
            SourceAccountsReceivableReceiptId: postedReplacementReceipt.AccountsReceivableReceiptId,
            RefundDate: new DateOnly(2026, 5, 27),
            RefundMethod: "BANK_TRANSFER",
            CurrencyCode: "EGP",
            AmountAmount: 80m,
            PaymentChannelConfigurationId: null,
            ReferenceNumber: "CUS-REVERSE-RFND-0001",
            Notes: "Refund after receipt reallocation."));
        refundService.SubmitRefund(createdRefund.CustomerRefundId, 3);
        refundService.ApproveRefund(createdRefund.CustomerRefundId, 4);
        var postedRefund = refundService.PostRefund(createdRefund.CustomerRefundId, 4);

        var portalPayments = externalPortalService.GetPayments(2);
        var portalLines = externalPortalService.GetStatementLines(2, new DateOnly(2026, 5, 1), new DateOnly(2026, 5, 31), "EGP");
        var portalSummary = externalPortalService.GetStatementSummary(2, new DateOnly(2026, 5, 1), new DateOnly(2026, 5, 31), "EGP");
        var statement = customerStatementService.GenerateStatement(3, new CreateCustomerStatementRequest(
            CustomerId: 2,
            DateFrom: new DateOnly(2026, 5, 1),
            DateTo: new DateOnly(2026, 5, 31),
            CurrencyCode: "EGP"));

        Assert.DoesNotContain(portalPayments, item => item.PaymentNumber == postedOriginalReceipt.ReceiptNumber);
        Assert.Contains(portalPayments, item => item.PaymentNumber == postedReplacementReceipt.ReceiptNumber && item.PaymentType == "AR_RECEIPT");
        Assert.Contains(portalPayments, item => item.PaymentNumber == postedRefund.RefundNumber && item.PaymentType == "CUSTOMER_REFUND");
        Assert.Contains(portalLines, item => item.DocumentNumber == postedReplacementReceipt.ReceiptNumber);
        Assert.Contains(portalLines, item => item.DocumentNumber == postedRefund.RefundNumber);
        Assert.Contains(statement.Lines, item => item.DocumentNumber == postedReplacementReceipt.ReceiptNumber);
        Assert.Contains(statement.Lines, item => item.DocumentNumber == postedRefund.RefundNumber);
        Assert.True(portalSummary.PaymentCount >= 2);
    }

    [Fact]
    public void Eta_Callback_Recovery_Remains_Synchronized_Across_Inbox_Processing_Monitoring_Transmissions_And_Internal_Exception_Clearing()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var integrationService = new IntegrationEndpointRegistryService(new IntegrationEndpointRegistryRepository(dbContext));
        CreateAndActivateEtaEndpoint(
            integrationService,
            "ETA outbound transport endpoint",
            "OUTBOUND",
            "REST",
            "POST",
            "/api/v1.0/transport/documentsubmissions",
            "BASIC",
            "inline://eta-signing-secret");
        var inboundEndpoint = CreateAndActivateEtaEndpoint(
            integrationService,
            "ETA document notifications endpoint",
            "INBOUND",
            "WEBHOOK",
            "PUT",
            "/api/v1.0/notifications/documents",
            "API_KEY",
            "inline://eta-callback-secret");

        var invoiceService = new AccountsReceivableInvoiceService(new AccountsReceivableInvoiceRepository(dbContext));
        var etaService = new EtaComplianceService(new EtaComplianceRepository(dbContext));
        var inboxService = new InboxMessageService(new InboxMessageRepository(dbContext));
        var internalWorkbenchService = new InternalWorkbenchService(new InternalWorkbenchRepository(dbContext));
        var authService = new EtaAuthorityAuthService(integrationService);
        var callbackService = new EtaAuthorityCallbackService(authService, etaService, new InboxMessageRepository(dbContext), inboxService);
        var notificationOperationsService = new EtaNotificationOperationsService(inboxService, callbackService, internalWorkbenchService);

        var postedInvoice = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostInvoice(
            invoiceService,
            invoiceService.CreateInvoice(new CreateAccountsReceivableInvoiceRequest(
                CustomerId: 2,
                SalesOrderId: 1,
                DispatchOrderId: 1,
                InvoiceType: "TAX",
                InvoiceDate: new DateOnly(2026, 5, 28),
                TaxDate: new DateOnly(2026, 5, 28),
                DueDate: new DateOnly(2026, 6, 7),
                CurrencyCode: "EGP",
                Lines:
                [
                    new CreateAccountsReceivableInvoiceLineRequest(1, 1001, 2m, "EA", 32m, "VAT14")
                ])).AccountsReceivableInvoiceId);

        var submission = etaService.SubmitInvoice(postedInvoice.AccountsReceivableInvoiceId, new SubmitInvoiceToEtaRequest(4, null));
        etaService.RecordInvoiceResponse(
            postedInvoice.AccountsReceivableInvoiceId,
            submission.EtaSubmissionLogId,
            new RecordEtaInvoiceResponseRequest(
                ActorUserId: 4,
                StatusCode: "SUBMITTED",
                EtaUuid: null,
                ResponseText: "{\"authoritySubmissionUuid\":\"SUB-ETA-CONN-0001\"}",
                ErrorCode: null,
                ErrorMessage: null,
                NotesText: "Linked for callback recovery continuity."));
        var queuedTransmission = etaService.QueueTransmission(postedInvoice.AccountsReceivableInvoiceId, submission.EtaSubmissionLogId, 4);
        var dispatchedTransmission = etaService.DispatchQueuedTransmissions(new DispatchEtaTransmissionBatchRequest("ETA_CONNECTOR", 10)).Transmissions.Single();
        Assert.Equal(queuedTransmission.EtaTransmissionAttemptId, dispatchedTransmission.EtaTransmissionAttemptId);

        var received = inboxService.ReceiveInboxMessage(1, new ReceiveInboxMessageRequest(
            inboundEndpoint.IntegrationEndpointId,
            "DOCUMENTRECEIVED",
            "ETA-CONN-NOTIF-0001",
            "SUB-ETA-CONN-0001",
            """
            {
              "notificationId": "ETA-CONN-NOTIF-0001",
              "type": "DocumentReceived",
              "notifications": [
                {
                  "submissionUUID": "SUB-ETA-CONN-0001",
                  "documentUUID": "DOC-ETA-CONN-0001",
                  "status": "Valid"
                }
              ]
            }
            """));

        var monitoringBefore = notificationOperationsService.GetMonitoring(10);
        Assert.True(monitoringBefore.ReceivedNotificationCount >= 1);
        Assert.Contains(monitoringBefore.RecentNotifications, item => item.InboxMessageId == received.InboxMessageId);

        var processed = notificationOperationsService.ProcessPendingNotifications(new ProcessEtaDocumentNotificationBatchRequest("ETA_NOTIFY_CONN", 10));
        Assert.Equal(1, processed.ProcessedCount);
        Assert.Equal(1, processed.ReconciledCount);
        Assert.Contains(received.InboxMessageId, processed.ProcessedInboxMessageIds);
        Assert.Contains(dispatchedTransmission.EtaTransmissionAttemptId, processed.ReconciledTransmissionIds);

        var transmission = etaService.GetTransmission(dispatchedTransmission.EtaTransmissionAttemptId);
        var monitoringAfter = notificationOperationsService.GetMonitoring(10);
        var etaExceptions = internalWorkbenchService.GetExceptionQueue("ETA");

        Assert.NotNull(transmission);
        Assert.Equal("ACCEPTED", transmission!.StatusCode);
        Assert.Equal(0, monitoringAfter.ReceivedNotificationCount);
        Assert.DoesNotContain(etaExceptions, item => item.DocumentId == postedInvoice.AccountsReceivableInvoiceId);
    }

    [Fact]
    public void Customer_Payment_Request_Callback_And_Settlement_Remain_Consistent_Between_Portal_Status_Payments_Statements_And_Executions()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var externalPortalService = CreateExternalPortalService(dbContext);
        var integrationService = new IntegrationEndpointRegistryService(new IntegrationEndpointRegistryRepository(dbContext));
        var paymentChannelService = new PaymentChannelConfigurationService(new PaymentChannelConfigurationRepository(dbContext));
        var executionService = new PaymentGatewayExecutionService(new PaymentGatewayExecutionRepository(dbContext));
        var callbackService = new GatewayCallbackService(new GatewayCallbackRepository(dbContext));
        var customerStatementService = new CustomerStatementService(new CustomerStatementRepository(dbContext));
        var invoiceService = new AccountsReceivableInvoiceService(new AccountsReceivableInvoiceRepository(dbContext));

        var outbound = CreateAndActivateEndpoint(integrationService, "Paymob collection outbound", "PAYMOB", "OUTBOUND", "/checkout");
        var inbound = CreateAndActivateEndpoint(integrationService, "Paymob collection callback", "PAYMOB", "INBOUND", "/checkout-callback");
        var channel = CreateAndActivateCollectionChannel(paymentChannelService, outbound.IntegrationEndpointId, inbound.IntegrationEndpointId);

        var postedInvoice = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostInvoice(
            invoiceService,
            invoiceService.CreateInvoice(new CreateAccountsReceivableInvoiceRequest(
                CustomerId: 2,
                SalesOrderId: 1,
                DispatchOrderId: 1,
                InvoiceType: "TAX",
                InvoiceDate: new DateOnly(2026, 5, 28),
                TaxDate: new DateOnly(2026, 5, 28),
                DueDate: new DateOnly(2026, 6, 7),
                CurrencyCode: "EGP",
                Lines:
                [
                    new CreateAccountsReceivableInvoiceLineRequest(1, 1001, 3m, "EA", 40m, "VAT14")
                ])).AccountsReceivableInvoiceId);

        var payment = externalPortalService.CreateCustomerPayment(2, 6, new CreateExternalCustomerPaymentRequest(
            AccountsReceivableInvoiceId: postedInvoice.AccountsReceivableInvoiceId,
            IntegrationProviderCode: "PAYMOB",
            CurrencyCode: "EGP",
            AmountAmount: postedInvoice.TotalAmount,
            PaymentDate: new DateOnly(2026, 5, 29),
            ReferenceNumber: "CUS-CONN-PAY-0001",
            Notes: "Connected customer payment request."));
        Assert.Equal("PENDING_CUSTOMER_ACTION", payment.StatusCode);

        var dispatchedExecution = executionService.DispatchPendingExecutions(new DispatchPaymentGatewayExecutionBatchRequest(
            ProcessorCode: "PAYMENT_EXEC_CONNECTOR",
            BatchSize: 10,
            PaymentChannelConfigurationId: channel.PaymentChannelConfigurationId)).Executions.Single();

        var payloadText = $$"""
        {
          "correlationKey": "{{dispatchedExecution.CorrelationKey}}",
          "providerReference": "{{dispatchedExecution.ProviderReference}}",
          "settlementStatus": "SETTLED",
          "settledAmount": {{postedInvoice.TotalAmount}},
          "notes": "Collection settled from connected flow."
        }
        """;
        var signature = GatewayCallbackSecurity.ComputeSignature("secret://paymob/uat/key", payloadText, "CUS-CONN-CB-0001", dispatchedExecution.CorrelationKey);

        var callbackReceived = callbackService.Receive(new GatewayCallbackReceiveRequest(
            channel.PaymentChannelConfigurationId,
            signature,
            "CUS-CONN-CB-0001",
            dispatchedExecution.CorrelationKey,
            "SETTLEMENT_STATUS",
            payloadText));
        Assert.Equal("ACCEPTED", callbackReceived.DecisionCode);

        var processed = callbackService.ProcessCallbacks(channel.PaymentChannelConfigurationId, new ProcessGatewayCallbackBatchRequest(
            ProcessorCode: "CALLBACK_CONNECTOR",
            BatchSize: 10));
        Assert.Equal(1, processed.ProcessedCount);
        Assert.Equal(1, processed.UpdatedExecutionCount);
        Assert.Equal(0, processed.SettlementMutationCount);

        var paymentStatus = externalPortalService.GetCustomerPaymentStatus(2, payment.PaymentId);
        var payments = externalPortalService.GetPayments(2);
        var statementLines = externalPortalService.GetStatementLines(2, new DateOnly(2026, 5, 1), new DateOnly(2026, 5, 31), "EGP");
        var statementSummary = externalPortalService.GetStatementSummary(2, new DateOnly(2026, 5, 1), new DateOnly(2026, 5, 31), "EGP");
        var statement = customerStatementService.GenerateStatement(3, new CreateCustomerStatementRequest(
            2,
            new DateOnly(2026, 5, 1),
            new DateOnly(2026, 5, 31),
            "EGP"));
        var executions = executionService.GetCustomerCollectionExecutions(payment.PaymentId);

        Assert.Equal("PAID", paymentStatus.StatusCode);
        Assert.Equal("SETTLED", paymentStatus.LatestExecutionStatusCode);
        Assert.Equal("SETTLED", paymentStatus.LatestProviderStatusCode);
        Assert.NotNull(paymentStatus.SettledAccountsReceivableReceiptId);
        Assert.Contains(payments, item => item.PaymentType == "CUSTOMER_PAYMENT_REQUEST" && item.PaymentId == payment.PaymentId && item.StatusCode == "PAID");
        Assert.Contains(payments, item => item.PaymentType == "AR_RECEIPT" && item.ReferenceNumber == dispatchedExecution.ProviderReference);
        Assert.Contains(statementLines, item => item.DocumentType == "AR_RECEIPT" && item.DocumentId == paymentStatus.SettledAccountsReceivableReceiptId);
        Assert.Contains(statement.Lines, item => item.DocumentId == paymentStatus.SettledAccountsReceivableReceiptId);
        Assert.True(statementSummary.PaymentCount >= 2);
        Assert.Single(executions);
        Assert.Equal("SETTLED", executions.Single().StatusCode);
    }

    [Fact]
    public void Customer_Service_Request_Escalation_Response_And_Closure_Remain_Consistent_Between_Portal_List_And_Detail_Views()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var externalPortalService = CreateExternalPortalService(dbContext);

        var created = externalPortalService.CreateCustomerServiceRequest(2, 6, new CreateExternalCustomerServiceRequestRequest(
            "DELIVERY_DISPUTE",
            "Need dispatch clarification",
            "Customer needs an update on a delayed dispatch.",
            "HIGH",
            "DELIVERY",
            "COMMERCIAL_REVIEW",
            "COMMERCIAL",
            "SALES_ORDER",
            1));
        var submitted = externalPortalService.SubmitCustomerServiceRequest(2, created.ServiceRequestId, 6);
        var escalated = externalPortalService.EscalateCustomerServiceRequest(2, created.ServiceRequestId, 6, new EscalateExternalCustomerServiceRequestRequest(
            "Escalate to finance for final commercial review.",
            "FINANCE",
            "CREDIT_REVIEW"));
        var routed = externalPortalService.RouteCustomerServiceRequest(created.ServiceRequestId, 3, new RouteExternalCustomerServiceRequestRequest(
            "COMMERCIAL",
            "COMMERCIAL_REVIEW"));
        var inReview = externalPortalService.StartCustomerServiceRequestReview(created.ServiceRequestId, 3);
        var responded = externalPortalService.RespondToCustomerServiceRequest(created.ServiceRequestId, 3, new ReviewExternalCustomerServiceRequest(
            "Dispatch has been rescheduled and the customer was notified."));
        var closed = externalPortalService.CloseCustomerServiceRequest(created.ServiceRequestId, 3, new CloseExternalCustomerServiceRequest(
            "Customer informed and case closed."));

        var listed = externalPortalService.GetCustomerServiceRequests(2);
        var detailed = listed.Single(item => item.ServiceRequestId == created.ServiceRequestId);

        Assert.Equal("SUBMITTED", submitted.StatusCode);
        Assert.Equal("ESCALATED", escalated.StatusCode);
        Assert.Equal(1, escalated.EscalationLevel);
        Assert.Equal("COMMERCIAL", routed.AssignedBackofficeLaneCode);
        Assert.Equal("IN_REVIEW", inReview.StatusCode);
        Assert.Equal("RESPONDED", responded.StatusCode);
        Assert.Equal("CLOSED", closed.StatusCode);
        Assert.Equal("CLOSED", detailed.StatusCode);
        Assert.Equal("Customer informed and case closed.", detailed.InternalResponseText);
        Assert.Equal("Escalate to finance for final commercial review.", detailed.EscalationReasonText);
        Assert.NotNull(detailed.ClosedAtUtc);
        Assert.Equal(3, detailed.ClosedByUserId);
    }

    [Fact]
    public void Supplier_Payout_Callback_Failure_And_Retry_Remain_Consistent_Between_Executions_Portal_Reconciliation_And_Reporting()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var externalPortalService = CreateExternalPortalService(dbContext);
        var integrationService = new IntegrationEndpointRegistryService(new IntegrationEndpointRegistryRepository(dbContext));
        var paymentChannelService = new PaymentChannelConfigurationService(new PaymentChannelConfigurationRepository(dbContext));
        var executionService = new PaymentGatewayExecutionService(new PaymentGatewayExecutionRepository(dbContext));
        var callbackService = new GatewayCallbackService(new GatewayCallbackRepository(dbContext));
        var apPaymentService = new AccountsPayablePaymentService(new AccountsPayablePaymentRepository(dbContext));
        var reconciliationService = new SupplierAccountReconciliationService(new SupplierAccountReconciliationRepository(dbContext));
        var reportingService = new CoreReportingService(new CoreReportingRepository(dbContext));

        var outbound = CreateAndActivateEndpoint(integrationService, "Fawry payout outbound", "FAWRY", "OUTBOUND", "/supplier-payouts");
        var inbound = CreateAndActivateEndpoint(integrationService, "Fawry payout callback", "FAWRY", "INBOUND", "/supplier-payout-callback");
        var payoutChannel = CreateAndActivatePayoutChannel(paymentChannelService, outbound.IntegrationEndpointId, inbound.IntegrationEndpointId, "FAWRY");

        var payment = apPaymentService.CreatePayment(new CreateAccountsPayablePaymentRequest(
            SupplierId: 1,
            PaymentDate: new DateOnly(2026, 5, 30),
            PaymentMethod: "BANK_TRANSFER",
            CurrencyCode: "EGP",
            AmountAmount: 42m,
            ReferenceNumber: "SUP-PAYOUT-CONN-0001",
            Notes: "Connected supplier payout",
            Allocations:
            [
                new CreateAccountsPayablePaymentAllocationRequest(1, 42m, new DateOnly(2026, 5, 30))
            ]));
        apPaymentService.SubmitPayment(payment.AccountsPayablePaymentId, 3);
        apPaymentService.ApprovePayment(payment.AccountsPayablePaymentId, 4);
        var postedPayment = apPaymentService.PostPayment(payment.AccountsPayablePaymentId, 4);

        var firstQueued = executionService.QueueSupplierPayout(postedPayment.AccountsPayablePaymentId, "FAWRY", 4);
        var firstDispatch = executionService.DispatchPendingExecutions(new DispatchPaymentGatewayExecutionBatchRequest(
            "SUPPLIER_PAYOUT_EXEC",
            10,
            payoutChannel.PaymentChannelConfigurationId)).Executions.Single();

        var failurePayloadText = $$"""
        {
          "correlationKey": "{{firstDispatch.CorrelationKey}}",
          "providerReference": "{{firstDispatch.ProviderReference}}",
          "settlementStatus": "FAILED",
          "notes": "Provider payout failed on first attempt."
        }
        """;
        var failureSignature = GatewayCallbackSecurity.ComputeSignature("secret://fawry/uat/key", failurePayloadText, "SUP-PAYOUT-CB-FAIL-0001", firstDispatch.CorrelationKey);
        var failureReceived = callbackService.Receive(new GatewayCallbackReceiveRequest(
            payoutChannel.PaymentChannelConfigurationId,
            failureSignature,
            "SUP-PAYOUT-CB-FAIL-0001",
            firstDispatch.CorrelationKey,
            "SETTLEMENT_STATUS",
            failurePayloadText));
        Assert.Equal("ACCEPTED", failureReceived.DecisionCode);

        var failedProcess = callbackService.ProcessCallbacks(payoutChannel.PaymentChannelConfigurationId, new ProcessGatewayCallbackBatchRequest(
            "SUPPLIER_PAYOUT_CALLBACK",
            10));
        Assert.Equal(1, failedProcess.ProcessedCount);

        var failedExecutions = executionService.GetSupplierPayoutExecutions(postedPayment.AccountsPayablePaymentId);
        Assert.Contains(failedExecutions, item => item.PaymentGatewayExecutionId == firstQueued.PaymentGatewayExecutionId && item.StatusCode == "FAILED");

        var retryQueued = executionService.QueueSupplierPayout(postedPayment.AccountsPayablePaymentId, "FAWRY", 4);
        var retryDispatch = executionService.DispatchPendingExecutions(new DispatchPaymentGatewayExecutionBatchRequest(
            "SUPPLIER_PAYOUT_EXEC_RETRY",
            10,
            payoutChannel.PaymentChannelConfigurationId)).Executions.Single(item => item.PaymentGatewayExecutionId == retryQueued.PaymentGatewayExecutionId);

        var successPayloadText = $$"""
        {
          "correlationKey": "{{retryDispatch.CorrelationKey}}",
          "providerReference": "{{retryDispatch.ProviderReference}}",
          "settlementStatus": "SETTLED",
          "settlementDate": "2026-05-30",
          "settledAmount": {{postedPayment.AmountAmount}},
          "feeAmount": 1.25,
          "netAmount": {{postedPayment.AmountAmount - 1.25m}},
          "externalBatchReference": "SUP-PAYOUT-SETTLED-0001"
        }
        """;
        var successSignature = GatewayCallbackSecurity.ComputeSignature("secret://fawry/uat/key", successPayloadText, "SUP-PAYOUT-CB-OK-0001", retryDispatch.CorrelationKey);
        var successReceived = callbackService.Receive(new GatewayCallbackReceiveRequest(
            payoutChannel.PaymentChannelConfigurationId,
            successSignature,
            "SUP-PAYOUT-CB-OK-0001",
            retryDispatch.CorrelationKey,
            "SETTLEMENT_STATUS",
            successPayloadText));
        Assert.Equal("ACCEPTED", successReceived.DecisionCode);

        var successProcess = callbackService.ProcessCallbacks(payoutChannel.PaymentChannelConfigurationId, new ProcessGatewayCallbackBatchRequest(
            "SUPPLIER_PAYOUT_CALLBACK_RETRY",
            10));
        Assert.Equal(1, successProcess.ProcessedCount);
        Assert.Equal(1, successProcess.SettlementMutationCount);

        var supplierPayments = externalPortalService.GetPayments(1);
        var reconciliation = reconciliationService.GetDetail(1, new DateOnly(2026, 5, 31), "EGP");
        var report = reportingService.GetCoreFinanceReport(new DateOnly(2026, 5, 1), new DateOnly(2026, 5, 31));
        var finalExecutions = executionService.GetSupplierPayoutExecutions(postedPayment.AccountsPayablePaymentId);

        Assert.Contains(supplierPayments, item => item.PaymentNumber == postedPayment.PaymentNumber);
        Assert.Contains(reconciliation.Lines, item => item.DocumentNumber == postedPayment.PaymentNumber);
        Assert.Contains(report.Rows, item => item.DocumentNumber == postedPayment.PaymentNumber && item.DocumentType == "AP_PAYMENT");
        Assert.Contains(finalExecutions, item => item.PaymentGatewayExecutionId == retryQueued.PaymentGatewayExecutionId && item.StatusCode == "SETTLED");
    }

    [Fact]
    public void Eta_Unreconciled_Callback_Can_Move_From_Retry_Backlog_To_Processed_After_Late_Transmission_Linkage()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var integrationService = new IntegrationEndpointRegistryService(new IntegrationEndpointRegistryRepository(dbContext));
        var inboundEndpoint = CreateAndActivateEtaEndpoint(
            integrationService,
            "ETA document notifications endpoint",
            "INBOUND",
            "WEBHOOK",
            "PUT",
            "/api/v1.0/notifications/documents",
            "API_KEY",
            "inline://eta-callback-secret");
        CreateAndActivateEtaEndpoint(
            integrationService,
            "ETA outbound transport endpoint",
            "OUTBOUND",
            "REST",
            "POST",
            "/api/v1.0/transport/documentsubmissions",
            "BASIC",
            "inline://eta-signing-secret");

        var inboxService = new InboxMessageService(new InboxMessageRepository(dbContext));
        var invoiceService = new AccountsReceivableInvoiceService(new AccountsReceivableInvoiceRepository(dbContext));
        var etaService = new EtaComplianceService(new EtaComplianceRepository(dbContext));
        var internalWorkbenchService = new InternalWorkbenchService(new InternalWorkbenchRepository(dbContext));
        var authService = new EtaAuthorityAuthService(integrationService);
        var callbackService = new EtaAuthorityCallbackService(authService, etaService, new InboxMessageRepository(dbContext), inboxService);
        var notificationOperationsService = new EtaNotificationOperationsService(inboxService, callbackService, internalWorkbenchService);

        var received = inboxService.ReceiveInboxMessage(1, new ReceiveInboxMessageRequest(
            inboundEndpoint.IntegrationEndpointId,
            "DOCUMENTRECEIVED",
            "ETA-LATE-LINK-0001",
            "SUB-ETA-LATE-0001",
            """
            {
              "notificationId": "ETA-LATE-LINK-0001",
              "type": "DocumentReceived",
              "notifications": [
                {
                  "submissionUUID": "SUB-ETA-LATE-0001",
                  "documentUUID": "DOC-ETA-LATE-0001",
                  "status": "Valid"
                }
              ]
            }
            """));

        var firstProcess = notificationOperationsService.ProcessPendingNotifications(new ProcessEtaDocumentNotificationBatchRequest("ETA_RETRY_CONN", 10));
        Assert.Equal(1, firstProcess.RetryCount);

        var monitoringAfterRetry = notificationOperationsService.GetMonitoring(10);
        Assert.True(monitoringAfterRetry.RetryNotificationCount >= 1);
        Assert.Contains(monitoringAfterRetry.RecentNotifications, item => item.InboxMessageId == received.InboxMessageId && item.StatusCode == "RETRY");

        var postedInvoice = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostInvoice(
            invoiceService,
            invoiceService.CreateInvoice(new CreateAccountsReceivableInvoiceRequest(
                CustomerId: 2,
                SalesOrderId: 1,
                DispatchOrderId: 1,
                InvoiceType: "TAX",
                InvoiceDate: new DateOnly(2026, 5, 31),
                TaxDate: new DateOnly(2026, 5, 31),
                DueDate: new DateOnly(2026, 6, 10),
                CurrencyCode: "EGP",
                Lines:
                [
                    new CreateAccountsReceivableInvoiceLineRequest(1, 1001, 1m, "EA", 32m, "VAT14")
                ])).AccountsReceivableInvoiceId);
        var submission = etaService.SubmitInvoice(postedInvoice.AccountsReceivableInvoiceId, new SubmitInvoiceToEtaRequest(4, null));
        etaService.RecordInvoiceResponse(
            postedInvoice.AccountsReceivableInvoiceId,
            submission.EtaSubmissionLogId,
            new RecordEtaInvoiceResponseRequest(
                ActorUserId: 4,
                StatusCode: "SUBMITTED",
                EtaUuid: null,
                ResponseText: "{\"authoritySubmissionUuid\":\"SUB-ETA-LATE-0001\"}",
                ErrorCode: null,
                ErrorMessage: null,
                NotesText: "Late linkage for ETA retry recovery."));
        etaService.QueueTransmission(postedInvoice.AccountsReceivableInvoiceId, submission.EtaSubmissionLogId, 4);
        etaService.DispatchQueuedTransmissions(new DispatchEtaTransmissionBatchRequest("ETA_LATE_LINK_EXEC", 10));

        var reprocessed = notificationOperationsService.ReprocessNotification(received.InboxMessageId, 4, new ReprocessEtaDocumentNotificationRequest("ETA_RETRY_REPROCESS", 0));
        var monitoringAfterRecovery = notificationOperationsService.GetMonitoring(10);
        var transmission = etaService.FindPendingTransmissionByCorrelation("SUB-ETA-LATE-0001");

        Assert.Equal("PROCESSED", reprocessed.StatusCode);
        Assert.True(reprocessed.Reconciled);
        Assert.Equal(0, monitoringAfterRecovery.RetryNotificationCount);
        Assert.Null(transmission);
    }

    [Fact]
    public void Customer_Payment_Failure_And_Retry_Remain_Consistent_Between_Portal_Status_Payments_And_Executions()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var externalPortalService = CreateExternalPortalService(dbContext);
        var integrationService = new IntegrationEndpointRegistryService(new IntegrationEndpointRegistryRepository(dbContext));
        var paymentChannelService = new PaymentChannelConfigurationService(new PaymentChannelConfigurationRepository(dbContext));
        var executionService = new PaymentGatewayExecutionService(new PaymentGatewayExecutionRepository(dbContext));
        var callbackService = new GatewayCallbackService(new GatewayCallbackRepository(dbContext));
        var invoiceService = new AccountsReceivableInvoiceService(new AccountsReceivableInvoiceRepository(dbContext));

        var outbound = CreateAndActivateEndpoint(integrationService, "Paymob collection outbound retry", "PAYMOB", "OUTBOUND", "/checkout-retry");
        var inbound = CreateAndActivateEndpoint(integrationService, "Paymob collection callback retry", "PAYMOB", "INBOUND", "/checkout-retry-callback");
        var channel = CreateAndActivateCollectionChannel(paymentChannelService, outbound.IntegrationEndpointId, inbound.IntegrationEndpointId, "PAYMOB");

        var postedInvoice = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostInvoice(
            invoiceService,
            invoiceService.CreateInvoice(new CreateAccountsReceivableInvoiceRequest(
                CustomerId: 2,
                SalesOrderId: 1,
                DispatchOrderId: 1,
                InvoiceType: "TAX",
                InvoiceDate: new DateOnly(2026, 5, 31),
                TaxDate: new DateOnly(2026, 5, 31),
                DueDate: new DateOnly(2026, 6, 10),
                CurrencyCode: "EGP",
                Lines:
                [
                    new CreateAccountsReceivableInvoiceLineRequest(1, 1001, 2m, "EA", 50m, "VAT14")
                ])).AccountsReceivableInvoiceId);

        var payment = externalPortalService.CreateCustomerPayment(2, 6, new CreateExternalCustomerPaymentRequest(
            postedInvoice.AccountsReceivableInvoiceId,
            "PAYMOB",
            "EGP",
            postedInvoice.TotalAmount,
            new DateOnly(2026, 5, 31),
            "CUS-PAY-FAIL-0001",
            "Customer payment with retry"));

        var firstDispatch = executionService.DispatchPendingExecutions(new DispatchPaymentGatewayExecutionBatchRequest(
            "CUSTOMER_PAY_EXEC",
            10,
            channel.PaymentChannelConfigurationId)).Executions.Single();

        var failedPayloadText = $$"""
        {
          "correlationKey": "{{firstDispatch.CorrelationKey}}",
          "providerReference": "{{firstDispatch.ProviderReference}}",
          "settlementStatus": "FAILED",
          "notes": "Payment declined by provider."
        }
        """;
        var failedSignature = GatewayCallbackSecurity.ComputeSignature("secret://paymob/uat/key", failedPayloadText, "CUS-PAY-FAIL-CB-0001", firstDispatch.CorrelationKey);
        callbackService.Receive(new GatewayCallbackReceiveRequest(
            channel.PaymentChannelConfigurationId,
            failedSignature,
            "CUS-PAY-FAIL-CB-0001",
            firstDispatch.CorrelationKey,
            "SETTLEMENT_STATUS",
            failedPayloadText));
        callbackService.ProcessCallbacks(channel.PaymentChannelConfigurationId, new ProcessGatewayCallbackBatchRequest("CUSTOMER_PAY_CALLBACK_FAIL", 10));

        var failedStatus = externalPortalService.GetCustomerPaymentStatus(2, payment.PaymentId);
        Assert.Equal("FAILED", failedStatus.StatusCode);
        Assert.Equal("FAILED", failedStatus.LatestExecutionStatusCode);

        var retryQueued = executionService.QueueCustomerCollection(payment.PaymentId, 6);
        var retryDispatch = executionService.DispatchPendingExecutions(new DispatchPaymentGatewayExecutionBatchRequest(
            "CUSTOMER_PAY_EXEC_RETRY",
            10,
            channel.PaymentChannelConfigurationId)).Executions.Single(item => item.PaymentGatewayExecutionId == retryQueued.PaymentGatewayExecutionId);

        var successPayloadText = $$"""
        {
          "correlationKey": "{{retryDispatch.CorrelationKey}}",
          "providerReference": "{{retryDispatch.ProviderReference}}",
          "settlementStatus": "SETTLED",
          "settledAmount": {{postedInvoice.TotalAmount}},
          "notes": "Payment settled after retry."
        }
        """;
        var successSignature = GatewayCallbackSecurity.ComputeSignature("secret://paymob/uat/key", successPayloadText, "CUS-PAY-OK-CB-0001", retryDispatch.CorrelationKey);
        callbackService.Receive(new GatewayCallbackReceiveRequest(
            channel.PaymentChannelConfigurationId,
            successSignature,
            "CUS-PAY-OK-CB-0001",
            retryDispatch.CorrelationKey,
            "SETTLEMENT_STATUS",
            successPayloadText));
        callbackService.ProcessCallbacks(channel.PaymentChannelConfigurationId, new ProcessGatewayCallbackBatchRequest("CUSTOMER_PAY_CALLBACK_OK", 10));

        var finalStatus = externalPortalService.GetCustomerPaymentStatus(2, payment.PaymentId);
        var payments = externalPortalService.GetPayments(2);
        var executions = executionService.GetCustomerCollectionExecutions(payment.PaymentId);

        Assert.Equal("PAID", finalStatus.StatusCode);
        Assert.Equal("SETTLED", finalStatus.LatestExecutionStatusCode);
        Assert.Contains(payments, item => item.PaymentId == payment.PaymentId && item.StatusCode == "PAID");
        Assert.Contains(executions, item => item.PaymentGatewayExecutionId == firstDispatch.PaymentGatewayExecutionId && item.StatusCode == "FAILED");
        Assert.Contains(executions, item => item.PaymentGatewayExecutionId == retryQueued.PaymentGatewayExecutionId && item.StatusCode == "SETTLED");
    }

    [Fact]
    public void Customer_Refund_Failure_And_Retry_Remain_Consistent_Between_Orchestration_Portal_Statements_And_Reporting()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var externalPortalService = CreateExternalPortalService(dbContext);
        var integrationService = new IntegrationEndpointRegistryService(new IntegrationEndpointRegistryRepository(dbContext));
        var paymentChannelService = new PaymentChannelConfigurationService(new PaymentChannelConfigurationRepository(dbContext));
        var executionService = new PaymentGatewayExecutionService(new PaymentGatewayExecutionRepository(dbContext));
        var callbackService = new GatewayCallbackService(new GatewayCallbackRepository(dbContext));
        var reportingService = new CoreReportingService(new CoreReportingRepository(dbContext));
        var refundService = new CustomerRefundService(new CustomerRefundRepository(dbContext), executionService);
        var receiptService = new AccountsReceivableReceiptService(new AccountsReceivableReceiptRepository(dbContext), refundService);
        var customerStatementService = new CustomerStatementService(new CustomerStatementRepository(dbContext));

        var channel = CreateAndActivateRefundChannel(integrationService, paymentChannelService);
        var postedRefund = CreatePostedRefund(receiptService, refundService, channel.PaymentChannelConfigurationId, 64m, "CUS-REF-RETRY-0001");

        var firstQueued = executionService.QueueCustomerRefund(postedRefund.CustomerRefundId, 4);
        var firstDispatch = executionService.DispatchPendingExecutions(new DispatchPaymentGatewayExecutionBatchRequest(
            "CUSTOMER_REFUND_EXEC",
            10,
            channel.PaymentChannelConfigurationId)).Executions.Single(item => item.PaymentGatewayExecutionId == firstQueued.PaymentGatewayExecutionId);

        var failedPayloadText = $$"""
        {
          "correlationKey": "{{firstDispatch.CorrelationKey}}",
          "providerReference": "{{firstDispatch.ProviderReference}}",
          "settlementStatus": "REJECTED",
          "notes": "Refund rejected by provider on first execution."
        }
        """;
        var failedSignature = GatewayCallbackSecurity.ComputeSignature("secret://paymob/uat/key", failedPayloadText, "CUS-REF-CB-FAIL-0001", firstDispatch.CorrelationKey);
        var failedReceived = callbackService.Receive(new GatewayCallbackReceiveRequest(
            channel.PaymentChannelConfigurationId,
            failedSignature,
            "CUS-REF-CB-FAIL-0001",
            firstDispatch.CorrelationKey,
            "SETTLEMENT_STATUS",
            failedPayloadText));
        Assert.Equal("ACCEPTED", failedReceived.DecisionCode);

        var failedProcess = callbackService.ProcessCallbacks(channel.PaymentChannelConfigurationId, new ProcessGatewayCallbackBatchRequest(
            "CUSTOMER_REFUND_CALLBACK_FAIL",
            10));
        Assert.Equal(1, failedProcess.ProcessedCount);

        var failedOrchestration = refundService.GetRefundOrchestration(postedRefund.CustomerRefundId);
        Assert.Equal(1, failedOrchestration.FailedExecutionCount);
        Assert.Equal("FAILED", failedOrchestration.LatestExecutionStatusCode);
        Assert.Equal("REJECTED", failedOrchestration.LatestProviderStatusCode);

        var retryQueued = executionService.QueueCustomerRefund(postedRefund.CustomerRefundId, 4);
        var retryDispatch = executionService.DispatchPendingExecutions(new DispatchPaymentGatewayExecutionBatchRequest(
            "CUSTOMER_REFUND_EXEC_RETRY",
            10,
            channel.PaymentChannelConfigurationId)).Executions.Single(item => item.PaymentGatewayExecutionId == retryQueued.PaymentGatewayExecutionId);

        var settledPayloadText = $$"""
        {
          "correlationKey": "{{retryDispatch.CorrelationKey}}",
          "providerReference": "{{retryDispatch.ProviderReference}}",
          "settlementStatus": "SETTLED",
          "settlementDate": "2026-05-31",
          "settledAmount": {{postedRefund.AmountAmount}},
          "feeAmount": 1.10,
          "netAmount": {{postedRefund.AmountAmount - 1.10m}},
          "externalBatchReference": "CUS-REF-SETTLED-0001",
          "notes": "Refund settled after retry."
        }
        """;
        var settledSignature = GatewayCallbackSecurity.ComputeSignature("secret://paymob/uat/key", settledPayloadText, "CUS-REF-CB-OK-0001", retryDispatch.CorrelationKey);
        var settledReceived = callbackService.Receive(new GatewayCallbackReceiveRequest(
            channel.PaymentChannelConfigurationId,
            settledSignature,
            "CUS-REF-CB-OK-0001",
            retryDispatch.CorrelationKey,
            "SETTLEMENT_STATUS",
            settledPayloadText));
        Assert.Equal("ACCEPTED", settledReceived.DecisionCode);

        var settledProcess = callbackService.ProcessCallbacks(channel.PaymentChannelConfigurationId, new ProcessGatewayCallbackBatchRequest(
            "CUSTOMER_REFUND_CALLBACK_OK",
            10));
        Assert.Equal(1, settledProcess.ProcessedCount);
        Assert.Equal(1, settledProcess.SettlementMutationCount);

        var finalOrchestration = refundService.GetRefundOrchestration(postedRefund.CustomerRefundId);
        var payments = externalPortalService.GetPayments(2);
        var portalLines = externalPortalService.GetStatementLines(2, new DateOnly(2026, 5, 1), new DateOnly(2026, 5, 31), "EGP");
        var statement = customerStatementService.GenerateStatement(3, new CreateCustomerStatementRequest(
            2,
            new DateOnly(2026, 5, 1),
            new DateOnly(2026, 5, 31),
            "EGP"));
        var report = reportingService.GetCoreFinanceReport(new DateOnly(2026, 5, 1), new DateOnly(2026, 5, 31));

        Assert.Equal(2, finalOrchestration.TotalExecutionCount);
        Assert.Equal(1, finalOrchestration.SettledExecutionCount);
        Assert.Equal(1, finalOrchestration.FailedExecutionCount);
        Assert.Equal("SETTLED", finalOrchestration.LatestExecutionStatusCode);
        Assert.Equal(retryDispatch.CorrelationKey, finalOrchestration.ProviderContract?.CorrelationKey);
        Assert.Contains(payments, item => item.PaymentType == "CUSTOMER_REFUND" && item.PaymentNumber == postedRefund.RefundNumber);
        Assert.Contains(portalLines, item => item.DocumentNumber == postedRefund.RefundNumber && item.DocumentType == "CUSTOMER_REFUND");
        Assert.Contains(statement.Lines, item => item.DocumentNumber == postedRefund.RefundNumber && item.DocumentType == "CUSTOMER_REFUND");
        Assert.Contains(report.Rows, item => item.DocumentType == "CUSTOMER_REFUND" && item.ReferenceText is not null && item.ReferenceText.Contains(postedRefund.RefundNumber, StringComparison.OrdinalIgnoreCase));
    }

    [Fact]
    public void Customer_Refund_Settlement_Exception_Appears_In_Internal_Workbench_And_Clears_After_Finance_Resolution()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var integrationService = new IntegrationEndpointRegistryService(new IntegrationEndpointRegistryRepository(dbContext));
        var paymentChannelService = new PaymentChannelConfigurationService(new PaymentChannelConfigurationRepository(dbContext));
        var executionService = new PaymentGatewayExecutionService(new PaymentGatewayExecutionRepository(dbContext));
        var callbackService = new GatewayCallbackService(new GatewayCallbackRepository(dbContext));
        var settlementService = new PaymentSettlementBatchService(new PaymentSettlementBatchRepository(dbContext));
        var internalWorkbenchService = new InternalWorkbenchService(new InternalWorkbenchRepository(dbContext));
        var refundService = new CustomerRefundService(new CustomerRefundRepository(dbContext), executionService);
        var receiptService = new AccountsReceivableReceiptService(new AccountsReceivableReceiptRepository(dbContext), refundService);

        var channel = CreateAndActivateRefundChannel(integrationService, paymentChannelService);
        var postedRefund = CreatePostedRefund(receiptService, refundService, channel.PaymentChannelConfigurationId, 91m, "CUS-REF-EXC-0001");

        var queued = executionService.QueueCustomerRefund(postedRefund.CustomerRefundId, 4);
        var dispatched = executionService.DispatchPendingExecutions(new DispatchPaymentGatewayExecutionBatchRequest(
            "CUSTOMER_REFUND_EXEC_EXCEPTION",
            10,
            channel.PaymentChannelConfigurationId)).Executions.Single(item => item.PaymentGatewayExecutionId == queued.PaymentGatewayExecutionId);

        var payloadText = $$"""
        {
          "correlationKey": "{{dispatched.CorrelationKey}}",
          "providerReference": null,
          "settlementStatus": "SETTLED",
          "settlementDate": "2026-05-31",
          "settledAmount": {{postedRefund.AmountAmount - 6m}},
          "feeAmount": 2.00,
          "netAmount": {{postedRefund.AmountAmount - 8m}},
          "externalBatchReference": "CUS-REF-EXC-BATCH-0001",
          "notes": "Variance callback without provider reference."
        }
        """;
        var signature = GatewayCallbackSecurity.ComputeSignature("secret://paymob/uat/key", payloadText, "CUS-REF-EXC-CB-0001", dispatched.CorrelationKey);
        var received = callbackService.Receive(new GatewayCallbackReceiveRequest(
            channel.PaymentChannelConfigurationId,
            signature,
            "CUS-REF-EXC-CB-0001",
            dispatched.CorrelationKey,
            "SETTLEMENT_STATUS",
            payloadText));
        Assert.Equal("ACCEPTED", received.DecisionCode);

        var processed = callbackService.ProcessCallbacks(channel.PaymentChannelConfigurationId, new ProcessGatewayCallbackBatchRequest(
            "CUSTOMER_REFUND_CALLBACK_EXCEPTION",
            10));
        Assert.Equal(1, processed.ProcessedCount);
        Assert.Equal(1, processed.SettlementMutationCount);

        var openExceptions = settlementService.GetExceptions(statusCode: "OPEN")
            .Where(item => item.SourceDocumentType == "CUSTOMER_REFUND" && item.SourceDocumentId == postedRefund.CustomerRefundId)
            .ToArray();
        var exceptionQueue = internalWorkbenchService.GetExceptionQueue("PAYMENT_SETTLEMENT");
        var openAlerts = internalWorkbenchService.GetOperationalAlerts("HIGH", "FINANCE");

        Assert.Single(openExceptions);
        Assert.Contains(exceptionQueue, item => item.DocumentType == "PAYMENT_SETTLEMENT_EXCEPTION" && item.DocumentNumber.Contains(postedRefund.RefundNumber, StringComparison.OrdinalIgnoreCase));
        Assert.Contains(openAlerts, item => item.AlertCode == "SETTLEMENT_EXCEPTION_OPEN");

        foreach (var openException in openExceptions)
        {
            if (string.Equals(openException.ExceptionCode, "AMOUNT_VARIANCE", StringComparison.OrdinalIgnoreCase))
            {
                settlementService.OverrideException(openException.PaymentSettlementExceptionId, 4, new ResolvePaymentSettlementExceptionRequest("Variance approved by finance."));
            }
            else
            {
                settlementService.ConfirmException(openException.PaymentSettlementExceptionId, 4, new ResolvePaymentSettlementExceptionRequest("Provider reference confirmed by treasury."));
            }
        }

        var clearedExceptions = settlementService.GetExceptions(statusCode: "OPEN")
            .Where(item => item.SourceDocumentType == "CUSTOMER_REFUND" && item.SourceDocumentId == postedRefund.CustomerRefundId)
            .ToArray();
        var clearedQueue = internalWorkbenchService.GetExceptionQueue("PAYMENT_SETTLEMENT");
        var clearedAlerts = internalWorkbenchService.GetOperationalAlerts("HIGH", "FINANCE");

        Assert.Empty(clearedExceptions);
        Assert.DoesNotContain(clearedQueue, item => item.DocumentType == "PAYMENT_SETTLEMENT_EXCEPTION" && item.DocumentNumber.Contains(postedRefund.RefundNumber, StringComparison.OrdinalIgnoreCase));
        Assert.DoesNotContain(clearedAlerts, item => item.AlertCode == "SETTLEMENT_EXCEPTION_OPEN");
    }

    [Fact]
    public void Supplier_Payment_Reversal_Replacement_And_Payout_Settlement_Remain_Consistent_Between_Portal_Reconciliation_And_Reporting()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var externalPortalService = CreateExternalPortalService(dbContext);
        var integrationService = new IntegrationEndpointRegistryService(new IntegrationEndpointRegistryRepository(dbContext));
        var paymentChannelService = new PaymentChannelConfigurationService(new PaymentChannelConfigurationRepository(dbContext));
        var executionService = new PaymentGatewayExecutionService(new PaymentGatewayExecutionRepository(dbContext));
        var callbackService = new GatewayCallbackService(new GatewayCallbackRepository(dbContext));
        var billService = new AccountsPayableBillService(new AccountsPayableBillRepository(dbContext));
        var paymentService = new AccountsPayablePaymentService(new AccountsPayablePaymentRepository(dbContext));
        var reconciliationService = new SupplierAccountReconciliationService(new SupplierAccountReconciliationRepository(dbContext));
        var reportingService = new CoreReportingService(new CoreReportingRepository(dbContext));

        var outbound = CreateAndActivateEndpoint(integrationService, "Fawry payout outbound replacement", "FAWRY", "OUTBOUND", "/supplier-payouts-replacement");
        var inbound = CreateAndActivateEndpoint(integrationService, "Fawry payout callback replacement", "FAWRY", "INBOUND", "/supplier-payouts-replacement-callback");
        var payoutChannel = CreateAndActivatePayoutChannel(paymentChannelService, outbound.IntegrationEndpointId, inbound.IntegrationEndpointId, "FAWRY");

        var bill = billService.CreateBill(new CreateAccountsPayableBillRequest(
            SupplierId: 1,
            PurchaseOrderId: 1,
            GoodsReceiptId: 1,
            BillDate: new DateOnly(2026, 5, 28),
            DueDate: new DateOnly(2026, 6, 5),
            CurrencyCode: "EGP",
            Lines:
            [
                new CreateAccountsPayableBillLineRequest(1, 1001, 50m, "EA", 24m, "VAT14")
            ]));
        billService.SubmitBill(bill.AccountsPayableBillId, 3);
        billService.ApproveMatch(bill.AccountsPayableBillId, 4);
        var postedBill = billService.PostBill(bill.AccountsPayableBillId, 4);

        var originalPayment = paymentService.PostPayment(
            paymentService.ApprovePayment(
                paymentService.SubmitPayment(
                    paymentService.CreatePayment(new CreateAccountsPayablePaymentRequest(
                        SupplierId: 1,
                        PaymentDate: new DateOnly(2026, 5, 29),
                        PaymentMethod: "BANK_TRANSFER",
                        CurrencyCode: "EGP",
                        AmountAmount: postedBill.TotalAmount,
                        ReferenceNumber: "SUP-PAY-REPL-0001",
                        Notes: "Original supplier payment to be replaced",
                        Allocations:
                        [
                            new CreateAccountsPayablePaymentAllocationRequest(postedBill.AccountsPayableBillId, postedBill.TotalAmount, new DateOnly(2026, 5, 29))
                        ])).AccountsPayablePaymentId,
                    3).AccountsPayablePaymentId,
                4).AccountsPayablePaymentId,
            4);

        var reversedPayment = paymentService.ReversePayment(
            originalPayment.AccountsPayablePaymentId,
            4,
            new ReverseAccountsPayablePaymentRequest(
                ReversalDate: new DateOnly(2026, 5, 30),
                ReasonCode: "BANK_ERROR",
                Notes: "Replacement payout is required."));

        var replacementDraft = paymentService.RepostPayment(
            reversedPayment.AccountsPayablePaymentId,
            4,
            new RepostAccountsPayablePaymentRequest(
                PaymentDate: new DateOnly(2026, 5, 30),
                PaymentMethod: "BANK_TRANSFER",
                ReferenceNumber: "SUP-PAY-REPL-0001-R1",
                Notes: "Replacement supplier payment",
                Allocations:
                [
                    new CreateAccountsPayablePaymentAllocationRequest(postedBill.AccountsPayableBillId, postedBill.TotalAmount, new DateOnly(2026, 5, 30))
                ]));

        var replacementPosted = paymentService.PostPayment(
            paymentService.ApprovePayment(
                paymentService.SubmitPayment(replacementDraft.AccountsPayablePaymentId, 3).AccountsPayablePaymentId,
                4).AccountsPayablePaymentId,
            4);

        var queued = executionService.QueueSupplierPayout(replacementPosted.AccountsPayablePaymentId, "FAWRY", 4);
        var dispatched = executionService.DispatchPendingExecutions(new DispatchPaymentGatewayExecutionBatchRequest(
            "SUPPLIER_PAYOUT_REPLACEMENT_EXEC",
            10,
            payoutChannel.PaymentChannelConfigurationId)).Executions.Single(item => item.PaymentGatewayExecutionId == queued.PaymentGatewayExecutionId);

        var payloadText = $$"""
        {
          "correlationKey": "{{dispatched.CorrelationKey}}",
          "providerReference": "{{dispatched.ProviderReference}}",
          "settlementStatus": "SETTLED",
          "settlementDate": "2026-05-30",
          "settledAmount": {{replacementPosted.AmountAmount}},
          "feeAmount": 1.00,
          "netAmount": {{replacementPosted.AmountAmount - 1.00m}},
          "externalBatchReference": "SUP-PAY-REPL-BATCH-0001"
        }
        """;
        var signature = GatewayCallbackSecurity.ComputeSignature("secret://fawry/uat/key", payloadText, "SUP-PAY-REPL-CB-0001", dispatched.CorrelationKey);
        var received = callbackService.Receive(new GatewayCallbackReceiveRequest(
            payoutChannel.PaymentChannelConfigurationId,
            signature,
            "SUP-PAY-REPL-CB-0001",
            dispatched.CorrelationKey,
            "SETTLEMENT_STATUS",
            payloadText));
        Assert.Equal("ACCEPTED", received.DecisionCode);

        var processed = callbackService.ProcessCallbacks(payoutChannel.PaymentChannelConfigurationId, new ProcessGatewayCallbackBatchRequest(
            "SUPPLIER_PAYOUT_REPLACEMENT_CALLBACK",
            10));
        Assert.Equal(1, processed.ProcessedCount);
        Assert.Equal(1, processed.SettlementMutationCount);

        var supplierPayments = externalPortalService.GetPayments(1);
        var reconciliation = reconciliationService.GetDetail(1, new DateOnly(2026, 5, 31), "EGP");
        var report = reportingService.GetCoreFinanceReport(new DateOnly(2026, 5, 1), new DateOnly(2026, 5, 31));
        var refreshedOriginal = paymentService.GetPayment(originalPayment.AccountsPayablePaymentId);

        Assert.NotNull(refreshedOriginal);
        Assert.Equal("REVERSED", refreshedOriginal!.StatusCode);
        Assert.Equal(replacementPosted.AccountsPayablePaymentId, refreshedOriginal.RepostPaymentId);
        Assert.Contains(supplierPayments, item => item.PaymentType == "AP_PAYMENT" && item.PaymentNumber == replacementPosted.PaymentNumber);
        Assert.DoesNotContain(supplierPayments, item => item.PaymentType == "AP_PAYMENT" && item.PaymentNumber == originalPayment.PaymentNumber);
        Assert.Contains(reconciliation.Lines, item => item.DocumentNumber == replacementPosted.PaymentNumber);
        Assert.Contains(report.Rows, item => item.DocumentNumber == replacementPosted.PaymentNumber && item.DocumentType == "AP_PAYMENT");
    }

    [Fact]
    public void Fawry_Collection_Cancel_And_Status_Contracts_Follow_Failure_Retry_And_Settlement_Lifecycle()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var externalPortalService = CreateExternalPortalService(dbContext);
        var integrationService = new IntegrationEndpointRegistryService(new IntegrationEndpointRegistryRepository(dbContext));
        var paymentChannelService = new PaymentChannelConfigurationService(new PaymentChannelConfigurationRepository(dbContext));
        var executionService = new PaymentGatewayExecutionService(new PaymentGatewayExecutionRepository(dbContext));
        var callbackService = new GatewayCallbackService(new GatewayCallbackRepository(dbContext));
        var invoiceService = new AccountsReceivableInvoiceService(new AccountsReceivableInvoiceRepository(dbContext));

        var outbound = CreateAndActivateEndpoint(integrationService, "Fawry collection outbound contract lifecycle", "FAWRY", "OUTBOUND", "/collect-contract-lifecycle");
        var inbound = CreateAndActivateEndpoint(integrationService, "Fawry collection callback contract lifecycle", "FAWRY", "INBOUND", "/collect-contract-lifecycle-callback");
        var channel = CreateAndActivateCollectionChannel(paymentChannelService, outbound.IntegrationEndpointId, inbound.IntegrationEndpointId, "FAWRY");

        var postedInvoice = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostInvoice(
            invoiceService,
            invoiceService.CreateInvoice(new CreateAccountsReceivableInvoiceRequest(
                CustomerId: 2,
                SalesOrderId: 1,
                DispatchOrderId: 1,
                InvoiceType: "TAX",
                InvoiceDate: new DateOnly(2026, 5, 31),
                TaxDate: new DateOnly(2026, 5, 31),
                DueDate: new DateOnly(2026, 6, 10),
                CurrencyCode: "EGP",
                Lines:
                [
                    new CreateAccountsReceivableInvoiceLineRequest(1, 1001, 2m, "EA", 37m, "VAT14")
                ])).AccountsReceivableInvoiceId);

        var payment = externalPortalService.CreateCustomerPayment(2, 6, new CreateExternalCustomerPaymentRequest(
            postedInvoice.AccountsReceivableInvoiceId,
            "FAWRY",
            "EGP",
            postedInvoice.TotalAmount,
            new DateOnly(2026, 5, 31),
            "CUS-FAWRY-RETRY-0001",
            "Customer Fawry payment retry lifecycle"));

        var queuedExecution = executionService.GetCustomerCollectionExecutions(payment.PaymentId).Single();
        var firstDispatch = executionService.DispatchPendingExecutions(new DispatchPaymentGatewayExecutionBatchRequest(
            "FAWRY_COLLECTION_EXEC",
            10,
            channel.PaymentChannelConfigurationId)).Executions.Single(item => item.PaymentGatewayExecutionId == queuedExecution.PaymentGatewayExecutionId);

        var firstStatusContract = executionService.GetExecutionProviderStatusContract(firstDispatch.PaymentGatewayExecutionId);
        var firstCancelContract = executionService.GetExecutionProviderCancelContract(firstDispatch.PaymentGatewayExecutionId);

        Assert.Equal("FAWRY_PAYMENT_STATUS_V2", firstStatusContract.ContractTypeCode);
        Assert.Equal("FAWRY_CANCEL_UNPAID_ORDER", firstCancelContract.ContractTypeCode);
        Assert.Equal(firstDispatch.CorrelationKey, firstCancelContract.CorrelationKey);
        Assert.Contains(firstDispatch.ProviderReference!, firstCancelContract.RequestBodyJson);

        var declinedPayloadText = $$"""
        {
          "correlationKey": "{{firstDispatch.CorrelationKey}}",
          "providerReference": "{{firstDispatch.ProviderReference}}",
          "settlementStatus": "DECLINED",
          "notes": "Customer payment was declined by provider."
        }
        """;
        var declinedSignature = GatewayCallbackSecurity.ComputeSignature("secret://fawry/uat/key", declinedPayloadText, "CUS-FAWRY-CB-FAIL-0001", firstDispatch.CorrelationKey);
        callbackService.Receive(new GatewayCallbackReceiveRequest(
            channel.PaymentChannelConfigurationId,
            declinedSignature,
            "CUS-FAWRY-CB-FAIL-0001",
            firstDispatch.CorrelationKey,
            "SETTLEMENT_STATUS",
            declinedPayloadText));
        callbackService.ProcessCallbacks(channel.PaymentChannelConfigurationId, new ProcessGatewayCallbackBatchRequest("FAWRY_COLLECTION_CALLBACK_FAIL", 10));

        var failedStatus = externalPortalService.GetCustomerPaymentStatus(2, payment.PaymentId);
        Assert.Equal("FAILED", failedStatus.StatusCode);
        Assert.Equal("FAILED", failedStatus.LatestExecutionStatusCode);

        var retryQueued = executionService.QueueCustomerCollection(payment.PaymentId, 6);
        var retryDispatch = executionService.DispatchPendingExecutions(new DispatchPaymentGatewayExecutionBatchRequest(
            "FAWRY_COLLECTION_EXEC_RETRY",
            10,
            channel.PaymentChannelConfigurationId)).Executions.Single(item => item.PaymentGatewayExecutionId == retryQueued.PaymentGatewayExecutionId);

        var retryStatusContract = executionService.GetExecutionProviderStatusContract(retryDispatch.PaymentGatewayExecutionId);
        var retryCancelContract = executionService.GetExecutionProviderCancelContract(retryDispatch.PaymentGatewayExecutionId);

        Assert.Equal("FAWRY_PAYMENT_STATUS_V2", retryStatusContract.ContractTypeCode);
        Assert.Equal("FAWRY_CANCEL_UNPAID_ORDER", retryCancelContract.ContractTypeCode);
        Assert.Equal(retryDispatch.CorrelationKey, retryCancelContract.CorrelationKey);
        Assert.NotEqual(firstDispatch.CorrelationKey, retryDispatch.CorrelationKey);
        Assert.Contains(retryDispatch.ProviderReference!, retryCancelContract.RequestBodyJson);

        var settledPayloadText = $$"""
        {
          "correlationKey": "{{retryDispatch.CorrelationKey}}",
          "providerReference": "{{retryDispatch.ProviderReference}}",
          "settlementStatus": "SETTLED",
          "settledAmount": {{postedInvoice.TotalAmount}},
          "notes": "Customer payment settled after retry."
        }
        """;
        var settledSignature = GatewayCallbackSecurity.ComputeSignature("secret://fawry/uat/key", settledPayloadText, "CUS-FAWRY-CB-OK-0001", retryDispatch.CorrelationKey);
        callbackService.Receive(new GatewayCallbackReceiveRequest(
            channel.PaymentChannelConfigurationId,
            settledSignature,
            "CUS-FAWRY-CB-OK-0001",
            retryDispatch.CorrelationKey,
            "SETTLEMENT_STATUS",
            settledPayloadText));
        callbackService.ProcessCallbacks(channel.PaymentChannelConfigurationId, new ProcessGatewayCallbackBatchRequest("FAWRY_COLLECTION_CALLBACK_OK", 10));

        var finalStatus = externalPortalService.GetCustomerPaymentStatus(2, payment.PaymentId);
        Assert.Equal("PAID", finalStatus.StatusCode);
        Assert.Equal("SETTLED", finalStatus.LatestExecutionStatusCode);
        Assert.Throws<InvalidOperationException>(() => executionService.GetExecutionProviderCancelContract(retryDispatch.PaymentGatewayExecutionId));
    }

    [Fact]
    public void Fawry_Portal_Status_Provider_Contract_Rolls_Forward_To_The_Latest_Retry_Execution()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var externalPortalService = CreateExternalPortalService(dbContext);
        var integrationService = new IntegrationEndpointRegistryService(new IntegrationEndpointRegistryRepository(dbContext));
        var paymentChannelService = new PaymentChannelConfigurationService(new PaymentChannelConfigurationRepository(dbContext));
        var executionService = new PaymentGatewayExecutionService(new PaymentGatewayExecutionRepository(dbContext));
        var callbackService = new GatewayCallbackService(new GatewayCallbackRepository(dbContext));
        var invoiceService = new AccountsReceivableInvoiceService(new AccountsReceivableInvoiceRepository(dbContext));

        var outbound = CreateAndActivateEndpoint(integrationService, "Fawry portal status rollover outbound", "FAWRY", "OUTBOUND", "/portal-status-rollover");
        var inbound = CreateAndActivateEndpoint(integrationService, "Fawry portal status rollover callback", "FAWRY", "INBOUND", "/portal-status-rollover-callback");
        var channel = CreateAndActivateCollectionChannel(paymentChannelService, outbound.IntegrationEndpointId, inbound.IntegrationEndpointId, "FAWRY");

        var postedInvoice = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostInvoice(
            invoiceService,
            invoiceService.CreateInvoice(new CreateAccountsReceivableInvoiceRequest(
                CustomerId: 2,
                SalesOrderId: 1,
                DispatchOrderId: 1,
                InvoiceType: "TAX",
                InvoiceDate: new DateOnly(2026, 5, 30),
                TaxDate: new DateOnly(2026, 5, 30),
                DueDate: new DateOnly(2026, 5, 31),
                CurrencyCode: "EGP",
                Lines:
                [
                    new CreateAccountsReceivableInvoiceLineRequest(1, 1001, 1m, "EA", 41m, "VAT14")
                ])).AccountsReceivableInvoiceId);

        var payment = externalPortalService.CreateCustomerPayment(2, 6, new CreateExternalCustomerPaymentRequest(
            postedInvoice.AccountsReceivableInvoiceId,
            "FAWRY",
            "EGP",
            postedInvoice.TotalAmount,
            new DateOnly(2026, 5, 31),
            "CUS-FAWRY-ROLLOVER-0001",
            "Portal status should follow the latest retry execution"));

        var firstDispatch = executionService.DispatchPendingExecutions(new DispatchPaymentGatewayExecutionBatchRequest(
            "FAWRY_PORTAL_STATUS_EXEC",
            10,
            channel.PaymentChannelConfigurationId)).Executions.Single();

        var firstStatus = externalPortalService.GetCustomerPaymentStatus(2, payment.PaymentId);
        Assert.Equal(firstDispatch.CorrelationKey, firstStatus.CorrelationKey);
        Assert.Equal(firstDispatch.CorrelationKey, firstStatus.ProviderContract.CorrelationKey);
        Assert.Equal(firstDispatch.ProviderReference, firstStatus.LatestProviderReference);

        var declinedPayloadText = $$"""
        {
          "correlationKey": "{{firstDispatch.CorrelationKey}}",
          "providerReference": "{{firstDispatch.ProviderReference}}",
          "settlementStatus": "DECLINED",
          "notes": "Initial Fawry attempt declined before retry."
        }
        """;
        var declinedSignature = GatewayCallbackSecurity.ComputeSignature("secret://fawry/uat/key", declinedPayloadText, "CUS-FAWRY-ROLLOVER-CB-FAIL-0001", firstDispatch.CorrelationKey);
        callbackService.Receive(new GatewayCallbackReceiveRequest(
            channel.PaymentChannelConfigurationId,
            declinedSignature,
            "CUS-FAWRY-ROLLOVER-CB-FAIL-0001",
            firstDispatch.CorrelationKey,
            "SETTLEMENT_STATUS",
            declinedPayloadText));
        callbackService.ProcessCallbacks(channel.PaymentChannelConfigurationId, new ProcessGatewayCallbackBatchRequest("FAWRY_PORTAL_STATUS_CALLBACK_FAIL", 10));

        var retryQueued = executionService.QueueCustomerCollection(payment.PaymentId, 6);
        var retryDispatch = executionService.DispatchPendingExecutions(new DispatchPaymentGatewayExecutionBatchRequest(
            "FAWRY_PORTAL_STATUS_EXEC_RETRY",
            10,
            channel.PaymentChannelConfigurationId)).Executions.Single(item => item.PaymentGatewayExecutionId == retryQueued.PaymentGatewayExecutionId);

        var retryStatus = externalPortalService.GetCustomerPaymentStatus(2, payment.PaymentId);
        Assert.Equal("FAILED", retryStatus.StatusCode);
        Assert.Equal("DISPATCHED", retryStatus.LatestExecutionStatusCode);
        Assert.Equal(retryDispatch.CorrelationKey, retryStatus.CorrelationKey);
        Assert.Equal(retryDispatch.CorrelationKey, retryStatus.ProviderContract.CorrelationKey);
        Assert.Equal(retryDispatch.ProviderReference, retryStatus.LatestProviderReference);
        Assert.NotEqual(firstDispatch.CorrelationKey, retryStatus.CorrelationKey);
    }

    [Fact]
    public void Supplier_Payout_Settlement_Exception_Is_Visible_To_Internal_Operations_And_Clears_After_Resolution()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var integrationService = new IntegrationEndpointRegistryService(new IntegrationEndpointRegistryRepository(dbContext));
        var paymentChannelService = new PaymentChannelConfigurationService(new PaymentChannelConfigurationRepository(dbContext));
        var executionService = new PaymentGatewayExecutionService(new PaymentGatewayExecutionRepository(dbContext));
        var callbackService = new GatewayCallbackService(new GatewayCallbackRepository(dbContext));
        var settlementService = new PaymentSettlementBatchService(new PaymentSettlementBatchRepository(dbContext));
        var internalWorkbenchService = new InternalWorkbenchService(new InternalWorkbenchRepository(dbContext));
        var paymentService = new AccountsPayablePaymentService(new AccountsPayablePaymentRepository(dbContext));

        var outbound = CreateAndActivateEndpoint(integrationService, "Fawry payout outbound exception lifecycle", "FAWRY", "OUTBOUND", "/supplier-payouts-exception");
        var inbound = CreateAndActivateEndpoint(integrationService, "Fawry payout callback exception lifecycle", "FAWRY", "INBOUND", "/supplier-payouts-exception-callback");
        var payoutChannel = CreateAndActivatePayoutChannel(paymentChannelService, outbound.IntegrationEndpointId, inbound.IntegrationEndpointId, "FAWRY");

        var postedPayment = paymentService.CreatePayment(new CreateAccountsPayablePaymentRequest(
            SupplierId: 1,
            PaymentDate: new DateOnly(2026, 5, 31),
            PaymentMethod: "BANK_TRANSFER",
            CurrencyCode: "EGP",
            AmountAmount: 48m,
            ReferenceNumber: "SUP-PAYOUT-EXC-0001",
            Notes: "Supplier payout settlement exception lifecycle",
            Allocations:
            [
                new CreateAccountsPayablePaymentAllocationRequest(1, 48m, new DateOnly(2026, 5, 31))
            ]));
        paymentService.SubmitPayment(postedPayment.AccountsPayablePaymentId, 3);
        paymentService.ApprovePayment(postedPayment.AccountsPayablePaymentId, 4);
        var posted = paymentService.PostPayment(postedPayment.AccountsPayablePaymentId, 4);

        var queued = executionService.QueueSupplierPayout(posted.AccountsPayablePaymentId, "FAWRY", 4);
        var dispatched = executionService.DispatchPendingExecutions(new DispatchPaymentGatewayExecutionBatchRequest(
            "SUPPLIER_PAYOUT_EXEC_EXCEPTION",
            10,
            payoutChannel.PaymentChannelConfigurationId)).Executions.Single(item => item.PaymentGatewayExecutionId == queued.PaymentGatewayExecutionId);

        var payloadText = $$"""
        {
          "correlationKey": "{{dispatched.CorrelationKey}}",
          "providerReference": null,
          "settlementStatus": "SETTLED",
          "settlementDate": "2026-05-31",
          "settledAmount": {{posted.AmountAmount - 4m}},
          "feeAmount": 1.25,
          "netAmount": {{posted.AmountAmount - 5.25m}},
          "externalBatchReference": "SUP-PAYOUT-EXC-BATCH-0001",
          "notes": "Supplier payout settled with missing reference and variance."
        }
        """;
        var signature = GatewayCallbackSecurity.ComputeSignature("secret://fawry/uat/key", payloadText, "SUP-PAYOUT-EXC-CB-0001", dispatched.CorrelationKey);
        callbackService.Receive(new GatewayCallbackReceiveRequest(
            payoutChannel.PaymentChannelConfigurationId,
            signature,
            "SUP-PAYOUT-EXC-CB-0001",
            dispatched.CorrelationKey,
            "SETTLEMENT_STATUS",
            payloadText));
        var processed = callbackService.ProcessCallbacks(payoutChannel.PaymentChannelConfigurationId, new ProcessGatewayCallbackBatchRequest(
            "SUPPLIER_PAYOUT_CALLBACK_EXCEPTION",
            10));

        var openExceptions = settlementService.GetExceptions(statusCode: "OPEN")
            .Where(item => item.SourceDocumentType == "AP_PAYMENT" && item.SourceDocumentId == posted.AccountsPayablePaymentId)
            .ToArray();
        var exceptionQueue = internalWorkbenchService.GetExceptionQueue("PAYMENT_SETTLEMENT");
        var openAlerts = internalWorkbenchService.GetOperationalAlerts("HIGH", "FINANCE");

        Assert.Equal(1, processed.SettlementMutationCount);
        Assert.Single(openExceptions);
        Assert.Contains(openExceptions, item => item.ExceptionCode == "AMOUNT_VARIANCE");
        Assert.Contains(exceptionQueue, item => item.DocumentType == "PAYMENT_SETTLEMENT_EXCEPTION" && item.DocumentNumber.Contains(posted.PaymentNumber, StringComparison.OrdinalIgnoreCase));
        Assert.Contains(openAlerts, item => item.AlertCode == "SETTLEMENT_EXCEPTION_OPEN");

        settlementService.OverrideException(openExceptions.Single(item => item.ExceptionCode == "AMOUNT_VARIANCE").PaymentSettlementExceptionId, 4, new ResolvePaymentSettlementExceptionRequest("Variance approved by finance."));

        var clearedQueue = internalWorkbenchService.GetExceptionQueue("PAYMENT_SETTLEMENT");
        var clearedAlerts = internalWorkbenchService.GetOperationalAlerts("HIGH", "FINANCE");
        Assert.DoesNotContain(clearedQueue, item => item.DocumentType == "PAYMENT_SETTLEMENT_EXCEPTION" && item.DocumentNumber.Contains(posted.PaymentNumber, StringComparison.OrdinalIgnoreCase));
        Assert.DoesNotContain(clearedAlerts, item => item.AlertCode == "SETTLEMENT_EXCEPTION_OPEN");
    }

    [Fact]
    public void Supplier_Payout_Settlement_Exception_Clearance_Allows_Batch_Posting_And_Preserves_Report_Traceability()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var integrationService = new IntegrationEndpointRegistryService(new IntegrationEndpointRegistryRepository(dbContext));
        var paymentChannelService = new PaymentChannelConfigurationService(new PaymentChannelConfigurationRepository(dbContext));
        var executionService = new PaymentGatewayExecutionService(new PaymentGatewayExecutionRepository(dbContext));
        var callbackService = new GatewayCallbackService(new GatewayCallbackRepository(dbContext));
        var settlementService = new PaymentSettlementBatchService(new PaymentSettlementBatchRepository(dbContext));
        var paymentService = new AccountsPayablePaymentService(new AccountsPayablePaymentRepository(dbContext));
        var reportingService = new CoreReportingService(new CoreReportingRepository(dbContext));

        var outbound = CreateAndActivateEndpoint(integrationService, "Fawry payout outbound posting lifecycle", "FAWRY", "OUTBOUND", "/supplier-payouts-posting");
        var inbound = CreateAndActivateEndpoint(integrationService, "Fawry payout callback posting lifecycle", "FAWRY", "INBOUND", "/supplier-payouts-posting-callback");
        var payoutChannel = CreateAndActivatePayoutChannel(paymentChannelService, outbound.IntegrationEndpointId, inbound.IntegrationEndpointId, "FAWRY");

        var createdPayment = paymentService.CreatePayment(new CreateAccountsPayablePaymentRequest(
            SupplierId: 1,
            PaymentDate: new DateOnly(2026, 5, 31),
            PaymentMethod: "BANK_TRANSFER",
            CurrencyCode: "EGP",
            AmountAmount: 44m,
            ReferenceNumber: "SUP-PAYOUT-POST-0001",
            Notes: "Supplier payout batch posting lifecycle",
            Allocations:
            [
                new CreateAccountsPayablePaymentAllocationRequest(1, 44m, new DateOnly(2026, 5, 31))
            ]));
        paymentService.SubmitPayment(createdPayment.AccountsPayablePaymentId, 3);
        paymentService.ApprovePayment(createdPayment.AccountsPayablePaymentId, 4);
        var postedPayment = paymentService.PostPayment(createdPayment.AccountsPayablePaymentId, 4);

        var queued = executionService.QueueSupplierPayout(postedPayment.AccountsPayablePaymentId, "FAWRY", 4);
        var dispatched = executionService.DispatchPendingExecutions(new DispatchPaymentGatewayExecutionBatchRequest(
            "SUPPLIER_PAYOUT_EXEC_POSTING",
            10,
            payoutChannel.PaymentChannelConfigurationId)).Executions.Single(item => item.PaymentGatewayExecutionId == queued.PaymentGatewayExecutionId);

        var payloadText = $$"""
        {
          "correlationKey": "{{dispatched.CorrelationKey}}",
          "providerReference": null,
          "settlementStatus": "SETTLED",
          "settlementDate": "2026-05-31",
          "settledAmount": {{postedPayment.AmountAmount - 3m}},
          "feeAmount": 1.00,
          "netAmount": {{postedPayment.AmountAmount - 4m}},
          "externalBatchReference": "SUP-PAYOUT-POST-BATCH-0001"
        }
        """;
        var signature = GatewayCallbackSecurity.ComputeSignature("secret://fawry/uat/key", payloadText, "SUP-PAYOUT-POST-CB-0001", dispatched.CorrelationKey);
        callbackService.Receive(new GatewayCallbackReceiveRequest(
            payoutChannel.PaymentChannelConfigurationId,
            signature,
            "SUP-PAYOUT-POST-CB-0001",
            dispatched.CorrelationKey,
            "SETTLEMENT_STATUS",
            payloadText));
        var processed = callbackService.ProcessCallbacks(payoutChannel.PaymentChannelConfigurationId, new ProcessGatewayCallbackBatchRequest(
            "SUPPLIER_PAYOUT_CALLBACK_POSTING",
            10));
        var batchId = Assert.Single(processed.SettlementBatchIds);

        settlementService.SubmitBatch(batchId, 3);
        settlementService.ApproveBatch(batchId, 4);
        Action postBlocked = () => settlementService.PostBatch(batchId, 4);
        var blockedPost = Assert.Throws<InvalidOperationException>(postBlocked);
        Assert.Contains("open settlement exceptions", blockedPost.Message, StringComparison.OrdinalIgnoreCase);

        var openExceptions = settlementService.GetExceptions(batchId, "OPEN");
        settlementService.OverrideException(openExceptions.Single(item => item.ExceptionCode == "AMOUNT_VARIANCE").PaymentSettlementExceptionId, 4, new ResolvePaymentSettlementExceptionRequest("Variance approved by finance."));

        var postedBatch = settlementService.PostBatch(batchId, 4);
        var report = reportingService.GetCoreFinanceReport(new DateOnly(2026, 5, 1), new DateOnly(2026, 5, 31));

        Assert.Equal("POSTED", postedBatch.StatusCode);
        Assert.Equal(0, postedBatch.OpenExceptionCount);
        Assert.Equal(1, postedBatch.ResolvedExceptionCount);
        Assert.Contains(report.Rows, item => item.DocumentType == "AP_PAYMENT" && item.DocumentNumber == postedPayment.PaymentNumber);
    }

    [Fact]
    public void Customer_Payment_Remains_Actionable_While_Dispatched_Without_Callback_And_Settles_Cleanly_When_Callback_Arrives_Later()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var externalPortalService = CreateExternalPortalService(dbContext);
        var integrationService = new IntegrationEndpointRegistryService(new IntegrationEndpointRegistryRepository(dbContext));
        var paymentChannelService = new PaymentChannelConfigurationService(new PaymentChannelConfigurationRepository(dbContext));
        var executionService = new PaymentGatewayExecutionService(new PaymentGatewayExecutionRepository(dbContext));
        var callbackService = new GatewayCallbackService(new GatewayCallbackRepository(dbContext));
        var invoiceService = new AccountsReceivableInvoiceService(new AccountsReceivableInvoiceRepository(dbContext));

        var outbound = CreateAndActivateEndpoint(integrationService, "Fawry late callback outbound", "FAWRY", "OUTBOUND", "/collect-late-callback");
        var inbound = CreateAndActivateEndpoint(integrationService, "Fawry late callback inbound", "FAWRY", "INBOUND", "/collect-late-callback-callback");
        var channel = CreateAndActivateCollectionChannel(paymentChannelService, outbound.IntegrationEndpointId, inbound.IntegrationEndpointId, "FAWRY");

        var postedInvoice = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostInvoice(
            invoiceService,
            invoiceService.CreateInvoice(new CreateAccountsReceivableInvoiceRequest(
                CustomerId: 2,
                SalesOrderId: 1,
                DispatchOrderId: 1,
                InvoiceType: "TAX",
                InvoiceDate: new DateOnly(2026, 5, 20),
                TaxDate: new DateOnly(2026, 5, 20),
                DueDate: new DateOnly(2026, 5, 25),
                CurrencyCode: "EGP",
                Lines:
                [
                    new CreateAccountsReceivableInvoiceLineRequest(1, 1001, 1m, "EA", 42m, "VAT14")
                ])).AccountsReceivableInvoiceId);

        var payment = externalPortalService.CreateCustomerPayment(2, 6, new CreateExternalCustomerPaymentRequest(
            postedInvoice.AccountsReceivableInvoiceId,
            "FAWRY",
            "EGP",
            postedInvoice.TotalAmount,
            new DateOnly(2026, 5, 25),
            "CUS-LATE-CB-0001",
            "Customer payment awaiting late callback"));

        var queuedExecution = executionService.GetCustomerCollectionExecutions(payment.PaymentId).Single();
        var dispatched = executionService.DispatchPendingExecutions(new DispatchPaymentGatewayExecutionBatchRequest(
            "FAWRY_LATE_CALLBACK_EXEC",
            10,
            channel.PaymentChannelConfigurationId)).Executions.Single(item => item.PaymentGatewayExecutionId == queuedExecution.PaymentGatewayExecutionId);

        var inFlightStatus = externalPortalService.GetCustomerPaymentStatus(2, payment.PaymentId);
        var statusContract = executionService.GetExecutionProviderStatusContract(dispatched.PaymentGatewayExecutionId);
        var cancelContract = executionService.GetExecutionProviderCancelContract(dispatched.PaymentGatewayExecutionId);

        Assert.Equal("PENDING_CUSTOMER_ACTION", inFlightStatus.StatusCode);
        Assert.Equal("DISPATCHED", inFlightStatus.LatestExecutionStatusCode);
        Assert.Equal(dispatched.CorrelationKey, inFlightStatus.CorrelationKey);
        Assert.Equal("FAWRY_PAYMENT_STATUS_V2", statusContract.ContractTypeCode);
        Assert.Equal("FAWRY_CANCEL_UNPAID_ORDER", cancelContract.ContractTypeCode);

        var settledPayloadText = $$"""
        {
          "correlationKey": "{{dispatched.CorrelationKey}}",
          "providerReference": "{{dispatched.ProviderReference}}",
          "settlementStatus": "SETTLED",
          "settledAmount": {{postedInvoice.TotalAmount}},
          "notes": "Late callback completed customer payment."
        }
        """;
        var settledSignature = GatewayCallbackSecurity.ComputeSignature("secret://fawry/uat/key", settledPayloadText, "CUS-LATE-CB-CALLBACK-0001", dispatched.CorrelationKey);
        callbackService.Receive(new GatewayCallbackReceiveRequest(
            channel.PaymentChannelConfigurationId,
            settledSignature,
            "CUS-LATE-CB-CALLBACK-0001",
            dispatched.CorrelationKey,
            "SETTLEMENT_STATUS",
            settledPayloadText));
        callbackService.ProcessCallbacks(channel.PaymentChannelConfigurationId, new ProcessGatewayCallbackBatchRequest("FAWRY_LATE_CALLBACK_PROCESSOR", 10));

        var settledStatus = externalPortalService.GetCustomerPaymentStatus(2, payment.PaymentId);
        var payments = externalPortalService.GetPayments(2);

        Assert.Equal("PAID", settledStatus.StatusCode);
        Assert.Equal("SETTLED", settledStatus.LatestExecutionStatusCode);
        Assert.Contains(payments, item => item.PaymentId == payment.PaymentId && item.StatusCode == "PAID");
        Assert.Throws<InvalidOperationException>(() => executionService.GetExecutionProviderCancelContract(dispatched.PaymentGatewayExecutionId));
    }

    [Fact]
    public void Failed_Overdue_Customer_Payment_Creates_Collections_Pressure_And_Clears_After_Retry_Settlement()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var externalPortalService = CreateExternalPortalService(dbContext);
        var integrationService = new IntegrationEndpointRegistryService(new IntegrationEndpointRegistryRepository(dbContext));
        var paymentChannelService = new PaymentChannelConfigurationService(new PaymentChannelConfigurationRepository(dbContext));
        var executionService = new PaymentGatewayExecutionService(new PaymentGatewayExecutionRepository(dbContext));
        var callbackService = new GatewayCallbackService(new GatewayCallbackRepository(dbContext));
        var invoiceService = new AccountsReceivableInvoiceService(new AccountsReceivableInvoiceRepository(dbContext));
        var collectionsService = new CollectionsWorkbenchService(new CollectionsWorkbenchRepository(dbContext));
        var internalWorkbenchService = new InternalWorkbenchService(new InternalWorkbenchRepository(dbContext));

        var outbound = CreateAndActivateEndpoint(integrationService, "Paymob overdue collection outbound", "PAYMOB", "OUTBOUND", "/overdue-collection");
        var inbound = CreateAndActivateEndpoint(integrationService, "Paymob overdue collection callback", "PAYMOB", "INBOUND", "/overdue-collection-callback");
        var channel = CreateAndActivateCollectionChannel(paymentChannelService, outbound.IntegrationEndpointId, inbound.IntegrationEndpointId, "PAYMOB");

        var postedInvoice = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostInvoice(
            invoiceService,
            invoiceService.CreateInvoice(new CreateAccountsReceivableInvoiceRequest(
                CustomerId: 2,
                SalesOrderId: 1,
                DispatchOrderId: 1,
                InvoiceType: "TAX",
                InvoiceDate: new DateOnly(2026, 4, 1),
                TaxDate: new DateOnly(2026, 4, 1),
                DueDate: new DateOnly(2026, 4, 10),
                CurrencyCode: "EGP",
                Lines:
                [
                    new CreateAccountsReceivableInvoiceLineRequest(1, 1001, 2m, "EA", 55m, "VAT14")
                ])).AccountsReceivableInvoiceId);

        var payment = externalPortalService.CreateCustomerPayment(2, 6, new CreateExternalCustomerPaymentRequest(
            postedInvoice.AccountsReceivableInvoiceId,
            "PAYMOB",
            "EGP",
            postedInvoice.TotalAmount,
            new DateOnly(2026, 4, 12),
            "CUS-OVERDUE-FAIL-0001",
            "Overdue payment failure and recovery"));

        var firstDispatch = executionService.DispatchPendingExecutions(new DispatchPaymentGatewayExecutionBatchRequest(
            "PAYMOB_OVERDUE_EXEC",
            10,
            channel.PaymentChannelConfigurationId)).Executions.Single();

        var failedPayloadText = $$"""
        {
          "correlationKey": "{{firstDispatch.CorrelationKey}}",
          "providerReference": "{{firstDispatch.ProviderReference}}",
          "settlementStatus": "FAILED",
          "notes": "Initial payment attempt failed."
        }
        """;
        var failedSignature = GatewayCallbackSecurity.ComputeSignature("secret://paymob/uat/key", failedPayloadText, "CUS-OVERDUE-FAIL-CB-0001", firstDispatch.CorrelationKey);
        callbackService.Receive(new GatewayCallbackReceiveRequest(
            channel.PaymentChannelConfigurationId,
            failedSignature,
            "CUS-OVERDUE-FAIL-CB-0001",
            firstDispatch.CorrelationKey,
            "SETTLEMENT_STATUS",
            failedPayloadText));
        callbackService.ProcessCallbacks(channel.PaymentChannelConfigurationId, new ProcessGatewayCallbackBatchRequest("PAYMOB_OVERDUE_CALLBACK_FAIL", 10));

        var collectionsSummary = collectionsService.GetSummary(new DateOnly(2026, 4, 30));
        var collectionsQueue = collectionsService.GetQueue(new DateOnly(2026, 4, 30), actionRequiredOnly: true);
        var collectionAlerts = internalWorkbenchService.GetOperationalAlerts("WARNING", "COLLECTIONS");
        var failedStatus = externalPortalService.GetCustomerPaymentStatus(2, payment.PaymentId);

        Assert.Equal("FAILED", failedStatus.StatusCode);
        Assert.True(collectionsSummary.OverdueInvoiceCount >= 1);
        Assert.Contains(collectionsQueue, item => item.AccountsReceivableInvoiceId == postedInvoice.AccountsReceivableInvoiceId);
        Assert.Contains(collectionAlerts, item => item.AlertCode == "OVERDUE_COLLECTIONS_ACTION");

        var retryQueued = executionService.QueueCustomerCollection(payment.PaymentId, 6);
        var retryDispatch = executionService.DispatchPendingExecutions(new DispatchPaymentGatewayExecutionBatchRequest(
            "PAYMOB_OVERDUE_EXEC_RETRY",
            10,
            channel.PaymentChannelConfigurationId)).Executions.Single(item => item.PaymentGatewayExecutionId == retryQueued.PaymentGatewayExecutionId);

        var settledPayloadText = $$"""
        {
          "correlationKey": "{{retryDispatch.CorrelationKey}}",
          "providerReference": "{{retryDispatch.ProviderReference}}",
          "settlementStatus": "SETTLED",
          "settledAmount": {{postedInvoice.TotalAmount}},
          "notes": "Recovered payment settled successfully."
        }
        """;
        var settledSignature = GatewayCallbackSecurity.ComputeSignature("secret://paymob/uat/key", settledPayloadText, "CUS-OVERDUE-OK-CB-0001", retryDispatch.CorrelationKey);
        callbackService.Receive(new GatewayCallbackReceiveRequest(
            channel.PaymentChannelConfigurationId,
            settledSignature,
            "CUS-OVERDUE-OK-CB-0001",
            retryDispatch.CorrelationKey,
            "SETTLEMENT_STATUS",
            settledPayloadText));
        callbackService.ProcessCallbacks(channel.PaymentChannelConfigurationId, new ProcessGatewayCallbackBatchRequest("PAYMOB_OVERDUE_CALLBACK_OK", 10));

        var clearedSummary = collectionsService.GetSummary(new DateOnly(2026, 4, 30));
        var clearedQueue = collectionsService.GetQueue(new DateOnly(2026, 4, 30), actionRequiredOnly: true);
        var clearedAlerts = internalWorkbenchService.GetOperationalAlerts("WARNING", "COLLECTIONS");
        var settledStatus = externalPortalService.GetCustomerPaymentStatus(2, payment.PaymentId);

        Assert.Equal("PAID", settledStatus.StatusCode);
        Assert.Equal(0, clearedSummary.OverdueInvoiceCount);
        Assert.DoesNotContain(clearedQueue, item => item.AccountsReceivableInvoiceId == postedInvoice.AccountsReceivableInvoiceId);
        Assert.DoesNotContain(clearedAlerts, item => item.AlertCode == "OVERDUE_COLLECTIONS_ACTION");
    }

    [Fact]
    public void Supplier_Payout_Settlement_Exception_Changes_Operational_Dashboard_And_Clears_After_Finance_Resolution()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var integrationService = new IntegrationEndpointRegistryService(new IntegrationEndpointRegistryRepository(dbContext));
        var paymentChannelService = new PaymentChannelConfigurationService(new PaymentChannelConfigurationRepository(dbContext));
        var executionService = new PaymentGatewayExecutionService(new PaymentGatewayExecutionRepository(dbContext));
        var callbackService = new GatewayCallbackService(new GatewayCallbackRepository(dbContext));
        var settlementService = new PaymentSettlementBatchService(new PaymentSettlementBatchRepository(dbContext));
        var internalWorkbenchService = new InternalWorkbenchService(new InternalWorkbenchRepository(dbContext));
        var paymentService = new AccountsPayablePaymentService(new AccountsPayablePaymentRepository(dbContext));

        var baselineDashboard = internalWorkbenchService.GetOperationalDashboard("Testing", "1.0.0.0");

        var outbound = CreateAndActivateEndpoint(integrationService, "Fawry payout dashboard outbound", "FAWRY", "OUTBOUND", "/supplier-payouts-dashboard");
        var inbound = CreateAndActivateEndpoint(integrationService, "Fawry payout dashboard callback", "FAWRY", "INBOUND", "/supplier-payouts-dashboard-callback");
        var payoutChannel = CreateAndActivatePayoutChannel(paymentChannelService, outbound.IntegrationEndpointId, inbound.IntegrationEndpointId, "FAWRY");

        var postedPayment = paymentService.CreatePayment(new CreateAccountsPayablePaymentRequest(
            SupplierId: 1,
            PaymentDate: new DateOnly(2026, 5, 30),
            PaymentMethod: "BANK_TRANSFER",
            CurrencyCode: "EGP",
            AmountAmount: 53m,
            ReferenceNumber: "SUP-PAYOUT-DASH-0001",
            Notes: "Supplier payout dashboard lifecycle",
            Allocations:
            [
                new CreateAccountsPayablePaymentAllocationRequest(1, 53m, new DateOnly(2026, 5, 30))
            ]));
        paymentService.SubmitPayment(postedPayment.AccountsPayablePaymentId, 3);
        paymentService.ApprovePayment(postedPayment.AccountsPayablePaymentId, 4);
        var posted = paymentService.PostPayment(postedPayment.AccountsPayablePaymentId, 4);

        var queued = executionService.QueueSupplierPayout(posted.AccountsPayablePaymentId, "FAWRY", 4);
        var dispatched = executionService.DispatchPendingExecutions(new DispatchPaymentGatewayExecutionBatchRequest(
            "SUPPLIER_PAYOUT_EXEC_DASHBOARD",
            10,
            payoutChannel.PaymentChannelConfigurationId)).Executions.Single(item => item.PaymentGatewayExecutionId == queued.PaymentGatewayExecutionId);

        var payloadText = $$"""
        {
          "correlationKey": "{{dispatched.CorrelationKey}}",
          "providerReference": null,
          "settlementStatus": "SETTLED",
          "settlementDate": "2026-05-30",
          "settledAmount": {{posted.AmountAmount - 7m}},
          "feeAmount": 1.25,
          "netAmount": {{posted.AmountAmount - 8.25m}},
          "externalBatchReference": "SUP-PAYOUT-DASH-BATCH-0001",
          "notes": "Supplier payout variance should surface in dashboard metrics."
        }
        """;
        var signature = GatewayCallbackSecurity.ComputeSignature("secret://fawry/uat/key", payloadText, "SUP-PAYOUT-DASH-CB-0001", dispatched.CorrelationKey);
        callbackService.Receive(new GatewayCallbackReceiveRequest(
            payoutChannel.PaymentChannelConfigurationId,
            signature,
            "SUP-PAYOUT-DASH-CB-0001",
            dispatched.CorrelationKey,
            "SETTLEMENT_STATUS",
            payloadText));
        callbackService.ProcessCallbacks(payoutChannel.PaymentChannelConfigurationId, new ProcessGatewayCallbackBatchRequest(
            "SUPPLIER_PAYOUT_CALLBACK_DASHBOARD",
            10));

        var exceptionDashboard = internalWorkbenchService.GetOperationalDashboard("Testing", "1.0.0.0");
        Assert.True(exceptionDashboard.OpenSettlementExceptionCount > baselineDashboard.OpenSettlementExceptionCount);
        Assert.True(exceptionDashboard.HighAlertCount >= baselineDashboard.HighAlertCount);

        var openExceptions = settlementService.GetExceptions(statusCode: "OPEN")
            .Where(item => item.SourceDocumentType == "AP_PAYMENT" && item.SourceDocumentId == posted.AccountsPayablePaymentId)
            .ToArray();
        settlementService.OverrideException(openExceptions.Single(item => item.ExceptionCode == "AMOUNT_VARIANCE").PaymentSettlementExceptionId, 4, new ResolvePaymentSettlementExceptionRequest("Variance approved by finance."));

        var clearedDashboard = internalWorkbenchService.GetOperationalDashboard("Testing", "1.0.0.0");
        Assert.Equal(baselineDashboard.OpenSettlementExceptionCount, clearedDashboard.OpenSettlementExceptionCount);
        Assert.True(clearedDashboard.HighAlertCount <= exceptionDashboard.HighAlertCount);
    }

    [Fact]
    public void Overdue_Collections_Pressure_Changes_Operational_Dashboard_And_Clears_After_Retry_Settlement()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var externalPortalService = CreateExternalPortalService(dbContext);
        var integrationService = new IntegrationEndpointRegistryService(new IntegrationEndpointRegistryRepository(dbContext));
        var paymentChannelService = new PaymentChannelConfigurationService(new PaymentChannelConfigurationRepository(dbContext));
        var executionService = new PaymentGatewayExecutionService(new PaymentGatewayExecutionRepository(dbContext));
        var callbackService = new GatewayCallbackService(new GatewayCallbackRepository(dbContext));
        var invoiceService = new AccountsReceivableInvoiceService(new AccountsReceivableInvoiceRepository(dbContext));
        var internalWorkbenchService = new InternalWorkbenchService(new InternalWorkbenchRepository(dbContext));

        var baselineDashboard = internalWorkbenchService.GetOperationalDashboard("Testing", "1.0.0.0");

        var outbound = CreateAndActivateEndpoint(integrationService, "Paymob overdue dashboard outbound", "PAYMOB", "OUTBOUND", "/overdue-dashboard");
        var inbound = CreateAndActivateEndpoint(integrationService, "Paymob overdue dashboard callback", "PAYMOB", "INBOUND", "/overdue-dashboard-callback");
        var channel = CreateAndActivateCollectionChannel(paymentChannelService, outbound.IntegrationEndpointId, inbound.IntegrationEndpointId, "PAYMOB");

        var postedInvoice = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostInvoice(
            invoiceService,
            invoiceService.CreateInvoice(new CreateAccountsReceivableInvoiceRequest(
                CustomerId: 2,
                SalesOrderId: 1,
                DispatchOrderId: 1,
                InvoiceType: "TAX",
                InvoiceDate: new DateOnly(2026, 4, 2),
                TaxDate: new DateOnly(2026, 4, 2),
                DueDate: new DateOnly(2026, 4, 9),
                CurrencyCode: "EGP",
                Lines:
                [
                    new CreateAccountsReceivableInvoiceLineRequest(1, 1001, 2m, "EA", 60m, "VAT14")
                ])).AccountsReceivableInvoiceId);

        var payment = externalPortalService.CreateCustomerPayment(2, 6, new CreateExternalCustomerPaymentRequest(
            postedInvoice.AccountsReceivableInvoiceId,
            "PAYMOB",
            "EGP",
            postedInvoice.TotalAmount,
            new DateOnly(2026, 4, 11),
            "CUS-OVERDUE-DASH-0001",
            "Overdue collections dashboard lifecycle"));

        var firstDispatch = executionService.DispatchPendingExecutions(new DispatchPaymentGatewayExecutionBatchRequest(
            "PAYMOB_OVERDUE_DASH_EXEC",
            10,
            channel.PaymentChannelConfigurationId)).Executions.Single();

        var failedPayloadText = $$"""
        {
          "correlationKey": "{{firstDispatch.CorrelationKey}}",
          "providerReference": "{{firstDispatch.ProviderReference}}",
          "settlementStatus": "FAILED",
          "notes": "Initial overdue collection attempt failed."
        }
        """;
        var failedSignature = GatewayCallbackSecurity.ComputeSignature("secret://paymob/uat/key", failedPayloadText, "CUS-OVERDUE-DASH-CB-FAIL-0001", firstDispatch.CorrelationKey);
        callbackService.Receive(new GatewayCallbackReceiveRequest(
            channel.PaymentChannelConfigurationId,
            failedSignature,
            "CUS-OVERDUE-DASH-CB-FAIL-0001",
            firstDispatch.CorrelationKey,
            "SETTLEMENT_STATUS",
            failedPayloadText));
        callbackService.ProcessCallbacks(channel.PaymentChannelConfigurationId, new ProcessGatewayCallbackBatchRequest("PAYMOB_OVERDUE_DASH_CALLBACK_FAIL", 10));

        var pressuredDashboard = internalWorkbenchService.GetOperationalDashboard("Testing", "1.0.0.0");
        Assert.True(pressuredDashboard.OverdueCollectionsCount > baselineDashboard.OverdueCollectionsCount);
        Assert.True(pressuredDashboard.WarningAlertCount >= baselineDashboard.WarningAlertCount);

        var retryQueued = executionService.QueueCustomerCollection(payment.PaymentId, 6);
        var retryDispatch = executionService.DispatchPendingExecutions(new DispatchPaymentGatewayExecutionBatchRequest(
            "PAYMOB_OVERDUE_DASH_EXEC_RETRY",
            10,
            channel.PaymentChannelConfigurationId)).Executions.Single(item => item.PaymentGatewayExecutionId == retryQueued.PaymentGatewayExecutionId);

        var settledPayloadText = $$"""
        {
          "correlationKey": "{{retryDispatch.CorrelationKey}}",
          "providerReference": "{{retryDispatch.ProviderReference}}",
          "settlementStatus": "SETTLED",
          "settledAmount": {{postedInvoice.TotalAmount}},
          "notes": "Recovered overdue collection settled successfully."
        }
        """;
        var settledSignature = GatewayCallbackSecurity.ComputeSignature("secret://paymob/uat/key", settledPayloadText, "CUS-OVERDUE-DASH-CB-OK-0001", retryDispatch.CorrelationKey);
        callbackService.Receive(new GatewayCallbackReceiveRequest(
            channel.PaymentChannelConfigurationId,
            settledSignature,
            "CUS-OVERDUE-DASH-CB-OK-0001",
            retryDispatch.CorrelationKey,
            "SETTLEMENT_STATUS",
            settledPayloadText));
        callbackService.ProcessCallbacks(channel.PaymentChannelConfigurationId, new ProcessGatewayCallbackBatchRequest("PAYMOB_OVERDUE_DASH_CALLBACK_OK", 10));

        var clearedDashboard = internalWorkbenchService.GetOperationalDashboard("Testing", "1.0.0.0");
        Assert.Equal(baselineDashboard.OverdueCollectionsCount, clearedDashboard.OverdueCollectionsCount);
        Assert.True(clearedDashboard.WarningAlertCount <= pressuredDashboard.WarningAlertCount);
    }

    [Fact]
    public void Dispatched_Customer_Payment_Without_Callback_Does_Not_Open_Finance_Exception_Read_Models_Before_Settlement()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var externalPortalService = CreateExternalPortalService(dbContext);
        var integrationService = new IntegrationEndpointRegistryService(new IntegrationEndpointRegistryRepository(dbContext));
        var paymentChannelService = new PaymentChannelConfigurationService(new PaymentChannelConfigurationRepository(dbContext));
        var executionService = new PaymentGatewayExecutionService(new PaymentGatewayExecutionRepository(dbContext));
        var invoiceService = new AccountsReceivableInvoiceService(new AccountsReceivableInvoiceRepository(dbContext));
        var internalWorkbenchService = new InternalWorkbenchService(new InternalWorkbenchRepository(dbContext));

        var baselineDashboard = internalWorkbenchService.GetOperationalDashboard("Testing", "1.0.0.0");

        var outbound = CreateAndActivateEndpoint(integrationService, "Fawry no callback outbound", "FAWRY", "OUTBOUND", "/fawry-no-callback");
        var inbound = CreateAndActivateEndpoint(integrationService, "Fawry no callback inbound", "FAWRY", "INBOUND", "/fawry-no-callback-callback");
        var channel = CreateAndActivateCollectionChannel(paymentChannelService, outbound.IntegrationEndpointId, inbound.IntegrationEndpointId, "FAWRY");

        var postedInvoice = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostInvoice(
            invoiceService,
            invoiceService.CreateInvoice(new CreateAccountsReceivableInvoiceRequest(
                CustomerId: 2,
                SalesOrderId: 1,
                DispatchOrderId: 1,
                InvoiceType: "TAX",
                InvoiceDate: new DateOnly(2026, 5, 23),
                TaxDate: new DateOnly(2026, 5, 23),
                DueDate: new DateOnly(2026, 5, 28),
                CurrencyCode: "EGP",
                Lines:
                [
                    new CreateAccountsReceivableInvoiceLineRequest(1, 1001, 1m, "EA", 39m, "VAT14")
                ])).AccountsReceivableInvoiceId);

        var payment = externalPortalService.CreateCustomerPayment(2, 6, new CreateExternalCustomerPaymentRequest(
            postedInvoice.AccountsReceivableInvoiceId,
            "FAWRY",
            "EGP",
            postedInvoice.TotalAmount,
            new DateOnly(2026, 5, 28),
            "CUS-NO-CALLBACK-0001",
            "Customer payment awaiting callback without settlement exception"));

        var queuedExecution = executionService.GetCustomerCollectionExecutions(payment.PaymentId).Single();
        var dispatched = executionService.DispatchPendingExecutions(new DispatchPaymentGatewayExecutionBatchRequest(
            "FAWRY_NO_CALLBACK_EXEC",
            10,
            channel.PaymentChannelConfigurationId)).Executions.Single(item => item.PaymentGatewayExecutionId == queuedExecution.PaymentGatewayExecutionId);

        var inFlightStatus = externalPortalService.GetCustomerPaymentStatus(2, payment.PaymentId);
        var inFlightDashboard = internalWorkbenchService.GetOperationalDashboard("Testing", "1.0.0.0");
        var financeExceptions = internalWorkbenchService.GetExceptionQueue("PAYMENT_SETTLEMENT");

        Assert.Equal("PENDING_CUSTOMER_ACTION", inFlightStatus.StatusCode);
        Assert.Equal("DISPATCHED", inFlightStatus.LatestExecutionStatusCode);
        Assert.Equal(baselineDashboard.OpenSettlementExceptionCount, inFlightDashboard.OpenSettlementExceptionCount);
        Assert.DoesNotContain(financeExceptions, item => item.DocumentNumber.Contains(dispatched.CorrelationKey, StringComparison.OrdinalIgnoreCase));
    }

    [Fact]
    public void Delayed_Fawry_Payment_Response_Keeps_User_Actionability_And_Provider_Status_Trace_Without_Raising_Finance_Exception_State()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var externalPortalService = CreateExternalPortalService(dbContext);
        var integrationService = new IntegrationEndpointRegistryService(new IntegrationEndpointRegistryRepository(dbContext));
        var paymentChannelService = new PaymentChannelConfigurationService(new PaymentChannelConfigurationRepository(dbContext));
        var executionService = new PaymentGatewayExecutionService(new PaymentGatewayExecutionRepository(dbContext));
        var internalWorkbenchService = new InternalWorkbenchService(new InternalWorkbenchRepository(dbContext));
        var invoiceService = new AccountsReceivableInvoiceService(new AccountsReceivableInvoiceRepository(dbContext));

        var baselineDashboard = internalWorkbenchService.GetOperationalDashboard("Testing", "1.0.0.0");

        var outbound = CreateAndActivateEndpoint(integrationService, "Fawry delayed response outbound", "FAWRY", "OUTBOUND", "/delayed-response");
        var inbound = CreateAndActivateEndpoint(integrationService, "Fawry delayed response callback", "FAWRY", "INBOUND", "/delayed-response-callback");
        var channel = CreateAndActivateCollectionChannel(paymentChannelService, outbound.IntegrationEndpointId, inbound.IntegrationEndpointId, "FAWRY");

        var postedInvoice = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostInvoice(
            invoiceService,
            invoiceService.CreateInvoice(new CreateAccountsReceivableInvoiceRequest(
                CustomerId: 2,
                SalesOrderId: 1,
                DispatchOrderId: 1,
                InvoiceType: "TAX",
                InvoiceDate: new DateOnly(2026, 5, 24),
                TaxDate: new DateOnly(2026, 5, 24),
                DueDate: new DateOnly(2026, 5, 29),
                CurrencyCode: "EGP",
                Lines:
                [
                    new CreateAccountsReceivableInvoiceLineRequest(1, 1001, 1m, "EA", 43m, "VAT14")
                ])).AccountsReceivableInvoiceId);

        var payment = externalPortalService.CreateCustomerPayment(2, 6, new CreateExternalCustomerPaymentRequest(
            postedInvoice.AccountsReceivableInvoiceId,
            "FAWRY",
            "EGP",
            postedInvoice.TotalAmount,
            new DateOnly(2026, 5, 29),
            "CUS-DELAYED-STATUS-0001",
            "Delayed Fawry response should remain actionable and traceable"));

        var queuedExecution = executionService.GetCustomerCollectionExecutions(payment.PaymentId).Single();
        var dispatched = executionService.DispatchPendingExecutions(new DispatchPaymentGatewayExecutionBatchRequest(
            "FAWRY_DELAYED_STATUS_EXEC",
            10,
            channel.PaymentChannelConfigurationId)).Executions.Single(item => item.PaymentGatewayExecutionId == queuedExecution.PaymentGatewayExecutionId);

        var statusContract = executionService.GetExecutionProviderStatusContract(dispatched.PaymentGatewayExecutionId);
        var portalStatus = externalPortalService.GetCustomerPaymentStatus(2, payment.PaymentId);
        var inFlightDashboard = internalWorkbenchService.GetOperationalDashboard("Testing", "1.0.0.0");
        var financeExceptions = internalWorkbenchService.GetExceptionQueue("PAYMENT_SETTLEMENT");
        var financeAlerts = internalWorkbenchService.GetOperationalAlerts("HIGH", "FINANCE");

        Assert.Equal("PENDING_CUSTOMER_ACTION", portalStatus.StatusCode);
        Assert.Equal("DISPATCHED", portalStatus.LatestExecutionStatusCode);
        Assert.Equal(dispatched.CorrelationKey, portalStatus.CorrelationKey);
        Assert.Equal(dispatched.CorrelationKey, portalStatus.ProviderContract.CorrelationKey);
        Assert.Equal("FAWRY_PAYMENT_STATUS_V2", statusContract.ContractTypeCode);
        Assert.Equal(dispatched.CorrelationKey, statusContract.CorrelationKey);
        Assert.Equal(baselineDashboard.OpenSettlementExceptionCount, inFlightDashboard.OpenSettlementExceptionCount);
        Assert.DoesNotContain(financeExceptions, item => item.ReferenceText == "FAWRY");
        Assert.DoesNotContain(financeAlerts, item => item.AlertCode == "SETTLEMENT_EXCEPTION_OPEN");
    }

    [Fact]
    public void Acknowledged_Fawry_Response_Moves_Portal_Status_To_Processing_And_Keeps_Unresolved_Contracts_Available()
    {
        using var dbContext = QaDbContextFactory.CreateContext();

        var externalPortalService = CreateExternalPortalService(dbContext);
        var integrationService = new IntegrationEndpointRegistryService(new IntegrationEndpointRegistryRepository(dbContext));
        var paymentChannelService = new PaymentChannelConfigurationService(new PaymentChannelConfigurationRepository(dbContext));
        var executionService = new PaymentGatewayExecutionService(new PaymentGatewayExecutionRepository(dbContext));
        var callbackService = new GatewayCallbackService(new GatewayCallbackRepository(dbContext));
        var internalWorkbenchService = new InternalWorkbenchService(new InternalWorkbenchRepository(dbContext));
        var invoiceService = new AccountsReceivableInvoiceService(new AccountsReceivableInvoiceRepository(dbContext));

        var baselineDashboard = internalWorkbenchService.GetOperationalDashboard("Testing", "1.0.0.0");

        var outbound = CreateAndActivateEndpoint(integrationService, "Fawry acknowledged outbound", "FAWRY", "OUTBOUND", "/acknowledged-status");
        var inbound = CreateAndActivateEndpoint(integrationService, "Fawry acknowledged callback", "FAWRY", "INBOUND", "/acknowledged-status-callback");
        var channel = CreateAndActivateCollectionChannel(paymentChannelService, outbound.IntegrationEndpointId, inbound.IntegrationEndpointId, "FAWRY");

        var postedInvoice = AccountsReceivableWorkflowTestHelper.SubmitApproveAndPostInvoice(
            invoiceService,
            invoiceService.CreateInvoice(new CreateAccountsReceivableInvoiceRequest(
                CustomerId: 2,
                SalesOrderId: 1,
                DispatchOrderId: 1,
                InvoiceType: "TAX",
                InvoiceDate: new DateOnly(2026, 5, 26),
                TaxDate: new DateOnly(2026, 5, 26),
                DueDate: new DateOnly(2026, 5, 30),
                CurrencyCode: "EGP",
                Lines:
                [
                    new CreateAccountsReceivableInvoiceLineRequest(1, 1001, 1m, "EA", 47m, "VAT14")
                ])).AccountsReceivableInvoiceId);

        var payment = externalPortalService.CreateCustomerPayment(2, 6, new CreateExternalCustomerPaymentRequest(
            postedInvoice.AccountsReceivableInvoiceId,
            "FAWRY",
            "EGP",
            postedInvoice.TotalAmount,
            new DateOnly(2026, 5, 30),
            "CUS-FAWRY-ACK-0001",
            "Acknowledged provider status should move portal state to processing"));

        var queuedExecution = executionService.GetCustomerCollectionExecutions(payment.PaymentId).Single();
        var dispatched = executionService.DispatchPendingExecutions(new DispatchPaymentGatewayExecutionBatchRequest(
            "FAWRY_ACK_EXEC",
            10,
            channel.PaymentChannelConfigurationId)).Executions.Single(item => item.PaymentGatewayExecutionId == queuedExecution.PaymentGatewayExecutionId);

        var acknowledgedPayloadText = $$"""
        {
          "correlationKey": "{{dispatched.CorrelationKey}}",
          "providerReference": "{{dispatched.ProviderReference}}",
          "settlementStatus": "PENDING",
          "notes": "Provider accepted the payment and is still awaiting final settlement."
        }
        """;
        var acknowledgedSignature = GatewayCallbackSecurity.ComputeSignature("secret://fawry/uat/key", acknowledgedPayloadText, "CUS-FAWRY-ACK-CB-0001", dispatched.CorrelationKey);
        callbackService.Receive(new GatewayCallbackReceiveRequest(
            channel.PaymentChannelConfigurationId,
            acknowledgedSignature,
            "CUS-FAWRY-ACK-CB-0001",
            dispatched.CorrelationKey,
            "SETTLEMENT_STATUS",
            acknowledgedPayloadText));
        callbackService.ProcessCallbacks(channel.PaymentChannelConfigurationId, new ProcessGatewayCallbackBatchRequest("FAWRY_ACK_CALLBACK", 10));

        var portalStatus = externalPortalService.GetCustomerPaymentStatus(2, payment.PaymentId);
        var statusContract = executionService.GetExecutionProviderStatusContract(dispatched.PaymentGatewayExecutionId);
        var cancelContract = executionService.GetExecutionProviderCancelContract(dispatched.PaymentGatewayExecutionId);
        var refreshedExecution = executionService.GetCustomerCollectionExecutions(payment.PaymentId).Single(item => item.PaymentGatewayExecutionId == dispatched.PaymentGatewayExecutionId);
        var dashboard = internalWorkbenchService.GetOperationalDashboard("Testing", "1.0.0.0");

        Assert.Equal("PROCESSING", portalStatus.StatusCode);
        Assert.Equal("ACKNOWLEDGED", portalStatus.LatestExecutionStatusCode);
        Assert.Equal("PENDING", portalStatus.LatestProviderStatusCode);
        Assert.Equal(dispatched.CorrelationKey, portalStatus.CorrelationKey);
        Assert.Equal(dispatched.CorrelationKey, portalStatus.ProviderContract.CorrelationKey);
        Assert.Equal("ACKNOWLEDGED", refreshedExecution.StatusCode);
        Assert.Equal("PENDING", refreshedExecution.ProviderStatusCode);
        Assert.Equal("FAWRY_PAYMENT_STATUS_V2", statusContract.ContractTypeCode);
        Assert.Equal("FAWRY_CANCEL_UNPAID_ORDER", cancelContract.ContractTypeCode);
        Assert.Equal(baselineDashboard.OpenSettlementExceptionCount, dashboard.OpenSettlementExceptionCount);
    }

    private static ExternalPortalService CreateExternalPortalService(CashLineErpDbContext dbContext)
    {
        var supplierProfileService = new SupplierProfileService(new SupplierProfileRepository(dbContext));
        var customerProfileService = new CustomerProfileService(new CustomerProfileRepository(dbContext));
        var partyOnboardingService = new PartyOnboardingService(new BusinessPartyRepository(dbContext));
        var salesOrderService = new SalesOrderService(new SalesOrderRepository(dbContext));
        var productService = new IslamicFinanceProductService(new IslamicFinanceProductRepository(dbContext));
        var notificationTemplateService = new NotificationTemplateService(new NotificationTemplateRepository(dbContext));
        var notificationOrchestrationService = new NotificationOrchestrationService(new NotificationOrchestrationRepository(dbContext));
        var paymentGatewayExecutionService = new PaymentGatewayExecutionService(new PaymentGatewayExecutionRepository(dbContext));
        var requestService = new IslamicFinanceRequestService(
            new IslamicFinanceRequestRepository(dbContext),
            notificationTemplateService,
            notificationOrchestrationService);
        var receivablesFinanceService = new ReceivablesFinanceService(new ReceivablesFinanceRepository(dbContext));
        var disbursementService = new IslamicFinanceMurabahaDisbursementService(new IslamicFinanceMurabahaDisbursementRepository(dbContext));
        var settlementService = new IslamicFinanceSettlementService(new IslamicFinanceSettlementRepository(dbContext));

        return new ExternalPortalService(
            new ExternalPortalRepository(dbContext),
            supplierProfileService,
            customerProfileService,
            partyOnboardingService,
            salesOrderService,
            productService,
            requestService,
            receivablesFinanceService,
            disbursementService,
            settlementService,
            notificationTemplateService,
            notificationOrchestrationService,
            paymentGatewayExecutionService);
    }

    private static ConnectedFlowHarness CreateHarness(CashLineErpDbContext dbContext)
    {
        var partyRepository = new BusinessPartyRepository(dbContext);
        var partyService = new PartyOnboardingService(partyRepository);
        var onboardingLeadService = new OnboardingLeadService(new OnboardingLeadRepository(dbContext));
        var notificationTemplateService = new NotificationTemplateService(new NotificationTemplateRepository(dbContext));
        var notificationOrchestrationService = new NotificationOrchestrationService(new NotificationOrchestrationRepository(dbContext));
        var provisioningService = new OnboardingAccessProvisioningService(
            new OnboardingAccessProvisioningRepository(dbContext),
            notificationTemplateService,
            notificationOrchestrationService,
            new IdentityReadRepository(dbContext),
            new SupplierProfileRepository(dbContext),
            new CustomerProfileRepository(dbContext),
            new AccountingOfficeClientAssignmentRepository(dbContext));
        var reviewGateService = new OnboardingReviewApprovalGateService(new OnboardingReviewRepository(dbContext));
        var supplierProfileService = new SupplierProfileService(new SupplierProfileRepository(dbContext), provisioningService, reviewGateService);
        var customerProfileService = new CustomerProfileService(new CustomerProfileRepository(dbContext), provisioningService, reviewGateService);
        var publicOnboardingService = new PublicOnboardingService(
            partyService,
            supplierProfileService,
            customerProfileService,
            onboardingLeadService);
        var internalOnboardingIntakeService = new InternalOnboardingIntakeService(
            partyService,
            supplierProfileService,
            customerProfileService,
            new IdentityReadRepository(dbContext));
        var onboardingCaseService = new OnboardingCaseService(new OnboardingCaseRepository(dbContext));
        var identityService = new IdentityService(new IdentityReadRepository(dbContext), new FakeAccessTokenService());

        return new ConnectedFlowHarness(
            partyService,
            onboardingLeadService,
            supplierProfileService,
            customerProfileService,
            publicOnboardingService,
            internalOnboardingIntakeService,
            onboardingCaseService,
            identityService);
    }

    private static string ExtractTemporaryPassword(string body)
    {
        var match = TemporaryPasswordRegex().Match(body);
        Assert.True(match.Success, $"Temporary password was not found in notification body: {body}");
        return match.Groups["password"].Value;
    }

    [GeneratedRegex(@"(?:Temporary password|Temp password):\s*(?<password>\S+)")]
    private static partial Regex TemporaryPasswordRegex();

    private static (long PartyId, long SupplierProfileId) CreateAndActivateSupplier(
        ConnectedFlowHarness services,
        string taxRegistrationNumber,
        string email,
        string mobile)
    {
        var application = services.PublicOnboardingService.CreateSupplierApplication(new CreatePublicSupplierOnboardingApplicationRequest(
            null,
            $"Supplier {taxRegistrationNumber}",
            "EG",
            null,
            taxRegistrationNumber,
            "001",
            email,
            mobile,
            "Cairo",
            "LOCAL_VENDOR",
            "EGP",
            "BANK_TRANSFER",
            30,
            5,
            1000m,
            "Banque Misr",
            "1111222233334444",
            "BMISEGCX",
            "Auto-activated supplier",
            [new RecordBusinessPartyComplianceDocumentRequest("COMMERCIAL_REGISTER", "commercial-register.pdf", $"managed://{taxRegistrationNumber}/commercial-register.pdf", "application/pdf", 2048)]));

        services.SupplierProfileService.ApproveSupplierProfile(application.SupplierProfileId, 4);
        services.SupplierProfileService.VerifySupplierBank(application.SupplierProfileId, 4, new VerifySupplierBankRequest($"BANK-{taxRegistrationNumber}"));
        services.SupplierProfileService.ActivateSupplierProfile(application.SupplierProfileId, 4);
        return (application.PartyId, application.SupplierProfileId);
    }

    private static (long PartyId, long CustomerProfileId) CreateAndActivateCustomer(
        ConnectedFlowHarness services,
        string taxRegistrationNumber,
        string email,
        string mobile)
    {
        var application = services.PublicOnboardingService.CreateCustomerApplication(new CreatePublicCustomerOnboardingApplicationRequest(
            null,
            $"Customer {taxRegistrationNumber}",
            "EG",
            null,
            taxRegistrationNumber,
            "001",
            email,
            mobile,
            "Alexandria",
            50000m,
            45,
            false,
            null,
            "COMPANY",
            "WHOLESALE",
            "EGP",
            "DIRECT_SALES",
            "STANDARD",
            "EMAIL",
            "Auto-activated customer",
            [new RecordBusinessPartyComplianceDocumentRequest("VAT_CERTIFICATE", "vat-certificate.pdf", $"managed://{taxRegistrationNumber}/vat-certificate.pdf", "application/pdf", 2048)]));

        services.CustomerProfileService.ApproveCustomerProfile(application.CustomerProfileId, 4);
        services.CustomerProfileService.ActivateCustomerProfile(application.CustomerProfileId, 4);
        return (application.PartyId, application.CustomerProfileId);
    }

    private static PaymentChannelConfigurationResponse CreateAndActivateRefundChannel(
        IntegrationEndpointRegistryService integrationService,
        PaymentChannelConfigurationService paymentChannelService,
        string currencyCode = "EGP")
    {
        var outbound = CreateAndActivateEndpoint(integrationService, "Paymob refund outbound", "PAYMOB", "OUTBOUND", "/refunds");
        var inbound = CreateAndActivateEndpoint(integrationService, "Paymob refund callback", "PAYMOB", "INBOUND", "/refund-callback");

        var created = paymentChannelService.CreatePaymentChannel(new CreatePaymentChannelConfigurationRequest(
            ChannelName: "Paymob refund channel",
            BranchId: 1,
            IntegrationProviderCode: "PAYMOB",
            ChannelTypeCode: "CHECKOUT_LINK",
            UsageCode: "CUSTOMER_REFUND",
            CurrencyCode: currencyCode,
            OutboundIntegrationEndpointId: outbound.IntegrationEndpointId,
            InboundIntegrationEndpointId: inbound.IntegrationEndpointId,
            SupportsCardTokenReferences: false,
            UsesWalletRuleEngine: false,
            RequiresCustomerReference: true,
            MinimumAmount: 1m,
            MaximumAmount: 5000m,
            SettlementLeadDays: 2,
            Notes: "Refund settlement channel"));

        paymentChannelService.SubmitPaymentChannel(created.PaymentChannelConfigurationId, 3);
        paymentChannelService.ApprovePaymentChannel(created.PaymentChannelConfigurationId, 4);
        return paymentChannelService.ActivatePaymentChannel(created.PaymentChannelConfigurationId, 4);
    }

    private static PaymentChannelConfigurationResponse CreateAndActivateCollectionChannel(
        PaymentChannelConfigurationService paymentChannelService,
        long outboundIntegrationEndpointId,
        long inboundIntegrationEndpointId,
        string providerCode = "PAYMOB",
        string currencyCode = "EGP")
    {
        var created = paymentChannelService.CreatePaymentChannel(new CreatePaymentChannelConfigurationRequest(
            ChannelName: $"{providerCode} customer collection channel",
            BranchId: 1,
            IntegrationProviderCode: providerCode,
            ChannelTypeCode: "CHECKOUT_LINK",
            UsageCode: "CUSTOMER_COLLECTION",
            CurrencyCode: currencyCode,
            OutboundIntegrationEndpointId: outboundIntegrationEndpointId,
            InboundIntegrationEndpointId: inboundIntegrationEndpointId,
            SupportsCardTokenReferences: false,
            UsesWalletRuleEngine: false,
            RequiresCustomerReference: true,
            MinimumAmount: 1m,
            MaximumAmount: 10000m,
            SettlementLeadDays: 2,
            Notes: "Connected customer collection channel"));

        paymentChannelService.SubmitPaymentChannel(created.PaymentChannelConfigurationId, 3);
        paymentChannelService.ApprovePaymentChannel(created.PaymentChannelConfigurationId, 4);
        return paymentChannelService.ActivatePaymentChannel(created.PaymentChannelConfigurationId, 4);
    }

    private static PaymentChannelConfigurationResponse CreateAndActivatePayoutChannel(
        PaymentChannelConfigurationService paymentChannelService,
        long outboundIntegrationEndpointId,
        long inboundIntegrationEndpointId,
        string providerCode,
        string currencyCode = "EGP")
    {
        var created = paymentChannelService.CreatePaymentChannel(new CreatePaymentChannelConfigurationRequest(
            ChannelName: $"{providerCode} supplier payout channel",
            BranchId: 1,
            IntegrationProviderCode: providerCode,
            ChannelTypeCode: "CHECKOUT_LINK",
            UsageCode: "SUPPLIER_PAYOUT",
            CurrencyCode: currencyCode,
            OutboundIntegrationEndpointId: outboundIntegrationEndpointId,
            InboundIntegrationEndpointId: inboundIntegrationEndpointId,
            SupportsCardTokenReferences: false,
            UsesWalletRuleEngine: false,
            RequiresCustomerReference: false,
            MinimumAmount: 1m,
            MaximumAmount: 10000m,
            SettlementLeadDays: 2,
            Notes: "Connected supplier payout channel"));

        paymentChannelService.SubmitPaymentChannel(created.PaymentChannelConfigurationId, 3);
        paymentChannelService.ApprovePaymentChannel(created.PaymentChannelConfigurationId, 4);
        return paymentChannelService.ActivatePaymentChannel(created.PaymentChannelConfigurationId, 4);
    }

    private static IntegrationEndpointResponse CreateAndActivateEndpoint(
        IntegrationEndpointRegistryService integrationService,
        string endpointName,
        string providerCode,
        string directionCode,
        string relativePath)
    {
        var created = integrationService.CreateIntegrationEndpoint(new CreateIntegrationEndpointRequest(
            EndpointName: endpointName,
            IntegrationProviderCode: providerCode,
            EnvironmentCode: "UAT",
            DirectionCode: directionCode,
            TransportCode: "REST",
            HttpMethodCode: "POST",
            BaseUrl: "https://gateway.uat.example",
            RelativePath: relativePath,
            AuthenticationModeCode: "SIGNATURE",
            CredentialReference: $"secret://{providerCode.ToLowerInvariant()}/uat/key",
            TimeoutSeconds: 20,
            RetryPolicyCode: "STANDARD",
            MaxRetryCount: 2,
            IsIdempotencyKeyRequired: true,
            Notes: "Connected settlement endpoint"));

        integrationService.SubmitIntegrationEndpoint(created.IntegrationEndpointId, 3);
        integrationService.ApproveIntegrationEndpoint(created.IntegrationEndpointId, 4);
        return integrationService.ActivateIntegrationEndpoint(created.IntegrationEndpointId, 4);
    }

    private static IntegrationEndpointResponse CreateAndActivateEtaEndpoint(
        IntegrationEndpointRegistryService integrationService,
        string endpointName,
        string directionCode,
        string transportCode,
        string httpMethodCode,
        string relativePath,
        string authenticationModeCode,
        string? credentialReference)
    {
        var created = integrationService.CreateIntegrationEndpoint(new CreateIntegrationEndpointRequest(
            EndpointName: endpointName,
            IntegrationProviderCode: "ETA",
            EnvironmentCode: "UAT",
            DirectionCode: directionCode,
            TransportCode: transportCode,
            HttpMethodCode: httpMethodCode,
            BaseUrl: "https://eta.uat.example",
            RelativePath: relativePath,
            AuthenticationModeCode: authenticationModeCode,
            CredentialReference: credentialReference,
            TimeoutSeconds: 30,
            RetryPolicyCode: "STANDARD",
            MaxRetryCount: 5,
            IsIdempotencyKeyRequired: true,
            Notes: "Connected ETA endpoint"));

        integrationService.SubmitIntegrationEndpoint(created.IntegrationEndpointId, 3);
        integrationService.ApproveIntegrationEndpoint(created.IntegrationEndpointId, 4);
        return integrationService.ActivateIntegrationEndpoint(created.IntegrationEndpointId, 4);
    }

    private static CustomerRefundResponse CreatePostedRefund(
        AccountsReceivableReceiptService receiptService,
        CustomerRefundService refundService,
        long paymentChannelConfigurationId,
        decimal amount,
        string referenceNumber,
        string currencyCode = "EGP")
    {
        var receipt = receiptService.CreateReceipt(new CreateAccountsReceivableReceiptRequest(
            CustomerId: 2,
            ReceiptDate: new DateOnly(2026, 5, 15),
            ReceiptMethod: "BANK_TRANSFER",
            CurrencyCode: currencyCode,
            AmountAmount: amount + 20m,
            ReferenceNumber: $"{referenceNumber}-RCPT",
            Notes: "Settlement source receipt",
            Allocations: []));

        receiptService.SubmitReceipt(receipt.AccountsReceivableReceiptId, 3);
        receiptService.ApproveReceipt(receipt.AccountsReceivableReceiptId, 4);
        var postedReceipt = receiptService.PostReceipt(receipt.AccountsReceivableReceiptId, 4);

        var refund = refundService.CreateRefund(new CreateCustomerRefundRequest(
            CustomerId: 2,
            SourceAccountsReceivableReceiptId: postedReceipt.AccountsReceivableReceiptId,
            RefundDate: new DateOnly(2026, 5, 15),
            RefundMethod: "PAYMENT_GATEWAY",
            CurrencyCode: currencyCode,
            AmountAmount: amount,
            PaymentChannelConfigurationId: paymentChannelConfigurationId,
            ReferenceNumber: referenceNumber,
            Notes: "Connected settlement refund"));

        refundService.SubmitRefund(refund.CustomerRefundId, 3);
        refundService.ApproveRefund(refund.CustomerRefundId, 4);
        return refundService.PostRefund(refund.CustomerRefundId, 4);
    }

    private sealed record ConnectedFlowHarness(
        PartyOnboardingService PartyOnboardingService,
        OnboardingLeadService OnboardingLeadService,
        SupplierProfileService SupplierProfileService,
        CustomerProfileService CustomerProfileService,
        PublicOnboardingService PublicOnboardingService,
        InternalOnboardingIntakeService InternalOnboardingIntakeService,
        OnboardingCaseService OnboardingCaseService,
        IdentityService IdentityService);
}
