{
  "format": "finci-partner-integration-test-plan",
  "format_version": "1.4",
  "api_version": "1.11.0",
  "guide": "/guides/integration-test-plan",
  "statuses": [
    "pending",
    "passed",
    "failed",
    "not_applicable"
  ],
  "instructions": [
    "Select tests using applies_when and your enabled program features. Record a reason for each not_applicable result.",
    "Every integration must validate the common transaction, reversal, retry and reference-handling checks even when the commercial use case is media buying.",
    "Use the complete masked downloads as local fixtures. Harness variations are explicitly simulated; preserve the published recorded messages.",
    "Run real API and network tests only in the FINCI-supported sandbox process using provisioned test data. Fictional example IDs are not provisioned cards.",
    "For each applicable test, record the observed hold and posted movement after every message, the exact references and outcome, and a link to sanitized evidence.",
    "A feature is not_applicable only when excluded by program configuration, not merely because it is absent from the expected customer journey."
  ],
  "required_configuration": [
    "Decisioning responsibility",
    "Card/account and currency mapping",
    "Enabled cash, FX, estimated-amount and additional-authorization features",
    "Amount-update rules by flow",
    "Hold expiry and other closing rules",
    "Permitted excesses and funding policy",
    "Receiver verification and response configuration",
    "Payments API access where ledger reconciliation is included"
  ],
  "tests": [
    {
      "test_id": "T01",
      "name": "Customer, account and card setup",
      "applies_when": "Every integration",
      "fixture_cases": [],
      "input_kind": "Provisioned sandbox API/configuration",
      "steps": [
        "Follow the personal or corporate issuance path enabled for the program.",
        "Create the initial customer with its required account object. For corporate cards, create the employee before the card.",
        "Issue the card with the provisioned product and save the returned identifiers."
      ],
      "expected_result": "The account maps to its person or company; token_id maps to the individual cardholder and configured account. No invented account_id or corporate_id is added to the create-card body.",
      "guide": "/guides/issuing-cards#choose-the-cardholder-path",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    },
    {
      "test_id": "T02",
      "name": "Status and stage",
      "applies_when": "Every integration",
      "fixture_cases": [],
      "input_kind": "Provisioned sandbox API/configuration",
      "steps": [
        "Read both status and stage.",
        "In the supported sandbox flow, pause with deactivated and reactivate with active.",
        "Close a separate test card with closed and confirm it cannot return to active. Validate the permanent-status restrictions for compromized, damaged, lost and stolen with FINCI.",
        "Where physical issuance applies, order with plastic_not_delivered and confirm pending delivery separately from status."
      ],
      "expected_result": "deactivated is temporary. closed, compromized, damaged, lost and stolen cannot return to active. plastic_not_delivered orders the physical card with delivery pending; status is unchanged. Existing holds and postings still follow their transaction rules.",
      "guide": "/guides/card-state-model#common-actions",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    },
    {
      "test_id": "T03",
      "name": "Exact IDs, money and account entries",
      "applies_when": "Every integration",
      "fixture_cases": [
        "E01",
        "E02"
      ],
      "input_kind": "Recorded masked webhook sequence",
      "steps": [
        "Parse, persist and read back each complete message.",
        "Compare id, token_id, references, decimal amounts and currencies with the downloaded text."
      ],
      "expected_result": "Every ID digit and string reference survives unchanged. Decimal money is exact. Every accounts_local entry is processed in its own currency.",
      "guide": "/guides/authorization-flow#preserve-precision",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    },
    {
      "test_id": "T04",
      "name": "Webhook verification and environment separation",
      "applies_when": "Every integration",
      "fixture_cases": [],
      "input_kind": "Test harness scenario",
      "steps": [
        "Send local test requests that satisfy the receiver verification configuration, then requests that fail it.",
        "Exercise sandbox and production configuration boundaries without sending production transactions."
      ],
      "expected_result": "The configured receiver accepts valid test traffic and rejects traffic that fails verification before a financial effect. Secrets and verification codes do not appear in logs.",
      "guide": "/guides/webhook-handling#webhook-endpoint-trust",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    },
    {
      "test_id": "T05",
      "name": "Approved purchase",
      "applies_when": "Every integration",
      "fixture_cases": [
        "E01"
      ],
      "input_kind": "Recorded masked webhook sequence",
      "steps": [
        "Deliver the request, approval confirmation and presentment in order.",
        "Record the hold and posted movement after each message."
      ],
      "expected_result": "Hold EUR 10.65, then keep EUR 10.65 with posted movement EUR 0.00. Presentment posts EUR -10.65 once and leaves hold EUR 0.00.",
      "guide": "/guides/transaction-lifecycle#purchase-eur-1065",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    },
    {
      "test_id": "T06",
      "name": "Declined purchase",
      "applies_when": "Every integration",
      "fixture_cases": [
        "E08"
      ],
      "input_kind": "Recorded masked webhook sequence",
      "steps": [
        "Deliver the EUR 135.60 request and its declined confirmation.",
        "For integrator-managed decisions, inspect the actual initial HTTP response: 403 with status error. Check the raw confirmed outcome and remaining reservation."
      ],
      "expected_result": "Initial decline is HTTP 403 with status error, not HTTP 200. Preserve confirmation response_code \"Insufficient funds\" and acknowledge its receipt. Release any provisional hold. Posted movement remains EUR 0.00.",
      "guide": "/guides/authorization-flow#declined-purchase",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    },
    {
      "test_id": "T07",
      "name": "Authorization timeout",
      "applies_when": "Integrator-managed decisions",
      "fixture_cases": [],
      "input_kind": "FINCI-supported sandbox session",
      "steps": [
        "In the FINCI-supported sandbox session, delay the initial reply beyond 3 seconds.",
        "Receive and process the resulting outcome confirmation."
      ],
      "expected_result": "Record the received outcome under the same authorization id; release any provisional hold if declined. A local timer alone does not fabricate a network outcome.",
      "guide": "/guides/authorization-flow#timeout-behavior",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    },
    {
      "test_id": "T08",
      "name": "Retries and concurrent decisions",
      "applies_when": "Every integration; concurrent decisions where integrator-managed",
      "fixture_cases": [
        "E01",
        "E05",
        "E09",
        "E10"
      ],
      "input_kind": "Test harness scenario",
      "steps": [
        "Repeat requests, confirmations, reversals and presentments, including simultaneous delivery.",
        "For integrator-managed decisions, run two requests against funds sufficient for only one."
      ],
      "expected_result": "Replay the saved initial response. Each hold change, reversal and posting occurs once. Concurrent approvals cannot reserve the same available funds twice.",
      "guide": "/guides/webhook-handling#deduplicate-the-effect-not-the-whole-lifecycle",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    },
    {
      "test_id": "T09",
      "name": "Request and confirmation sharing an ID",
      "applies_when": "Every integration",
      "fixture_cases": [
        "E01",
        "E08"
      ],
      "input_kind": "Recorded masked webhook sequence",
      "steps": [
        "Deliver both phases with the same id.",
        "Check the stored decision, confirmed outcome and accounting effects."
      ],
      "expected_result": "The confirmation is processed as a later phase rather than discarded as a duplicate. Negative confirmation amounts do not create postings; advice is not the phase classifier.",
      "guide": "/guides/authorization-flow#decision-sequence",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    },
    {
      "test_id": "T10",
      "name": "Partial clearing",
      "applies_when": "Every integration",
      "fixture_cases": [
        "E07"
      ],
      "input_kind": "Recorded masked webhook sequence",
      "steps": [
        "Approve EUR 6.50 and deliver EUR 6.00 clearing.",
        "Confirm that the first matched presentment releases the full EUR 6.50 hold. Replay the presentment and verify no second posting or hold release."
      ],
      "expected_result": "Posted movement is EUR -6.00; the full EUR 6.50 hold is released. Remaining hold is EUR 0.00 and EUR 0.50 becomes available again. No separate expiry or reversal is needed.",
      "guide": "/guides/clearing-and-reconciliation#smaller-presentment-and-hold-release",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    },
    {
      "test_id": "T11",
      "name": "Several presentments",
      "applies_when": "Every integration",
      "fixture_cases": [
        "E06"
      ],
      "input_kind": "Recorded masked webhook sequence",
      "steps": [
        "Deliver the approval, then both distinct EUR 2.90 presentments. Check the hold after the first presentment and replay each clearing.",
        "Apply the configured policy for any amount not covered by the hold."
      ],
      "expected_result": "The first presentment releases the entire EUR 0.01 hold. Post both presentments once: total EUR -5.80; hold remains EUR 0.00. Matching authorization_id and amount do not make the second record a duplicate. This fixture does not define a permitted excess limit.",
      "guide": "/guides/clearing-and-reconciliation#several-presentments",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    },
    {
      "test_id": "T12",
      "name": "Full reversal",
      "applies_when": "Every integration",
      "fixture_cases": [
        "E09"
      ],
      "input_kind": "Recorded masked webhook sequence",
      "steps": [
        "Deliver the approval and matched reversal.",
        "Repeat the reversal and inspect the remaining reservation and postings."
      ],
      "expected_result": "Store the reversal under its own id. Release up to the eligible EUR 2.55 hold, including the fee already in that total. No refund is posted.",
      "guide": "/guides/clearing-and-reconciliation#full-reversal",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    },
    {
      "test_id": "T13",
      "name": "Partial reversal",
      "applies_when": "Every integration",
      "fixture_cases": [
        "E05"
      ],
      "input_kind": "Recorded masked webhook sequence",
      "steps": [
        "Approve EUR 10.00, reverse EUR 2.46, then clear EUR 7.54.",
        "Check the hold after each step."
      ],
      "expected_result": "The supported unique rrn match links the reversal. Hold becomes EUR 7.54, then EUR 0.00; clearing posts EUR -7.54 once. Reversal id remains separate from the earlier authorization id.",
      "guide": "/guides/clearing-and-reconciliation#partial-reversal",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    },
    {
      "test_id": "T14",
      "name": "Explicit original reference with changed network references",
      "applies_when": "Every integration",
      "fixture_cases": [
        "E03"
      ],
      "input_kind": "Recorded masked webhook sequence",
      "steps": [
        "Compare the related authorization id and original_authorization_id with the earlier record.",
        "Check the different current rrn and nrn without assuming they must match."
      ],
      "expected_result": "original_authorization_id links the exact earlier id. original_authorization_nrn equals the earlier nrn. E03 is an approved additional authorization with advice false, not an advice message.",
      "guide": "/guides/authorization-flow#related-authorizations-and-matching",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    },
    {
      "test_id": "T15",
      "name": "Match through original_authorization_nrn",
      "applies_when": "Every integration",
      "fixture_cases": [
        "E05"
      ],
      "input_kind": "Test harness scenario",
      "steps": [
        "Configure the supported original_authorization_nrn matching scope and time window. In the test harness only, leave original_authorization_id empty and set original_authorization_nrn to the earlier approval's nrn.",
        "Repeat with two qualifying records, a missing explicit NRN target and records outside the configured scope."
      ],
      "expected_result": "One qualifying match resolves the link. A missing explicit target stays pending without RRN fallback; multiple candidates or out-of-scope records remain unresolved and do not release an unrelated hold. This is a simulated variation, not another recorded FINCI message.",
      "guide": "/guides/authorization-flow#related-authorizations-and-matching",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    },
    {
      "test_id": "T16",
      "name": "Pending and conflicting references",
      "applies_when": "Every integration",
      "fixture_cases": [
        "E03"
      ],
      "input_kind": "Test harness scenario",
      "steps": [
        "Deliver the related record before its referenced authorization.",
        "In the harness, create references that point to different records and a case with no unique supported match."
      ],
      "expected_result": "Missing explicit targets remain pending; no weaker fallback replaces them. Conflicts stay unresolved. Later resolution does not repeat the financial effect.",
      "guide": "/guides/authorization-flow#related-authorizations-and-matching",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    },
    {
      "test_id": "T17",
      "name": "Clearing references a related authorization",
      "applies_when": "Every integration",
      "fixture_cases": [
        "E03"
      ],
      "input_kind": "Test harness scenario",
      "steps": [
        "In the harness, make presentment authorization_id reference the related authorization.",
        "Deliver clearing before its original authorization chain is available, then resolve the chain."
      ],
      "expected_result": "Post once. After linkage resolves, close the whole matched reservation without reposting or creating a new hold for the already-cleared authorization.",
      "guide": "/guides/clearing-and-reconciliation#process-a-presentment",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    },
    {
      "test_id": "T18",
      "name": "Changed payload with the same event ID",
      "applies_when": "Every integration",
      "fixture_cases": [
        "E01"
      ],
      "input_kind": "Test harness scenario",
      "steps": [
        "Post the presentment once.",
        "In the harness, repeat its id and phase with a changed accounts_local entry."
      ],
      "expected_result": "Retain the changed-payload conflict. There is no second posting merely because the account array changed.",
      "guide": "/guides/webhook-handling#idempotency-and-retries",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    },
    {
      "test_id": "T19",
      "name": "Hold expiry and late events",
      "applies_when": "Every integration",
      "fixture_cases": [
        "E07"
      ],
      "input_kind": "Test harness scenario",
      "steps": [
        "Use the E07 authorization messages without delivering its presentment. With a controlled test clock, reach the configured expiry and release the full open EUR 6.50 hold. Cover the configured final-authorization and preauthorization windows, commonly 7 and 30 calendar days respectively.",
        "Confirm that three business days without clearing alone does not close a hold.",
        "Then deliver a late distinct presentment and a reversal against the expired reservation."
      ],
      "expected_result": "Apply configured expiry once. Post a late distinct clearing once without reopening an expired hold. A reversal after expiry or clearing releases nothing and does not refund a posted debit.",
      "guide": "/guides/clearing-and-reconciliation#hold-expiry-before-presentment",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    },
    {
      "test_id": "T20",
      "name": "Refund credit",
      "applies_when": "Every integration",
      "fixture_cases": [
        "E10"
      ],
      "input_kind": "Recorded masked webhook sequence",
      "steps": [
        "Deliver the Return authorization and approval, then its credit presentment.",
        "Check posting timing and the reference target."
      ],
      "expected_result": "Return approval does not post a refund. Presentment posts EUR +177.17 once; authorization_id refers to the return authorization, not automatically to the earlier purchase.",
      "guide": "/guides/clearing-and-reconciliation#refund-eur-17717",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    },
    {
      "test_id": "T21",
      "name": "Presentment without an authorization reference",
      "applies_when": "Every integration",
      "fixture_cases": [
        "E11"
      ],
      "input_kind": "Recorded masked webhook sequence",
      "steps": [
        "Deliver the clearing with empty authorization_id and replay it.",
        "Inspect the posting and unresolved relationship."
      ],
      "expected_result": "Post EUR -1.84 once and retain the clearing without inventing an authorization link. An empty reference alone does not prove an offline transaction.",
      "guide": "/guides/clearing-and-reconciliation#presentment-without-an-authorization-reference",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    },
    {
      "test_id": "T22",
      "name": "Multiple account allocations",
      "applies_when": "Programs with multiple local account allocations",
      "fixture_cases": [],
      "input_kind": "Test harness scenario",
      "steps": [
        "In the harness, authorize entries in EUR and GBP, then clear only the explicitly linked EUR entry.",
        "Inspect all allocations of the matched reservation and an unrelated authorization hold."
      ],
      "expected_result": "Post only the supplied EUR movement. Close the whole matched authorization reservation, including its EUR and GBP allocations; keep an unrelated authorization hold unchanged. Never combine different currencies. Authorization identity does not depend on identical account arrays.",
      "guide": "/guides/clearing-and-reconciliation#process-a-presentment",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    },
    {
      "test_id": "T23",
      "name": "FX purchase with included markup",
      "applies_when": "Programs that allow cross-currency purchases",
      "fixture_cases": [
        "E02"
      ],
      "input_kind": "Recorded masked webhook sequence",
      "steps": [
        "Deliver the USD 49.99 purchase sequence against the EUR account.",
        "Check that the original amount and currency remain available for purchase display; compare local amounts, rates and fees."
      ],
      "expected_result": "Keep USD 49.99 for original purchase display. Hold EUR 45.00, then post EUR -45.00 once. Presentment has Transaction EUR +44.78, Markup EUR -0.22 and account_local_rate 1; its Transaction currency matches account_currency EUR. The EUR 0.22 markup is included; do not reserve EUR 45.22 or post another fee.",
      "guide": "/guides/webhook-amounts#fx-purchase-with-markup",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    },
    {
      "test_id": "T24",
      "name": "ATM withdrawal with fees",
      "applies_when": "Programs that allow cash withdrawals",
      "fixture_cases": [
        "E12"
      ],
      "input_kind": "Recorded masked webhook sequence",
      "steps": [
        "Deliver the CHF 200.00 withdrawal sequence.",
        "Compare the supplied local total with the Transaction, Fee and Markup components."
      ],
      "expected_result": "Hold EUR 223.18, then post EUR -223.18 once. Fee is the EUR 6.88 ATM fee; Markup is EUR 1.08. They combine into local fee_amount EUR 7.96, already included in account_amount. Do not charge any component again.",
      "guide": "/guides/webhook-amounts#atm-withdrawal-with-fx-and-a-fee",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    },
    {
      "test_id": "T25",
      "name": "Completion advice for an estimated amount",
      "applies_when": "Programs that support estimated-amount completion advice, including fuel or taxi fares",
      "fixture_cases": [
        "E04"
      ],
      "input_kind": "Recorded masked webhook sequence",
      "steps": [
        "Use the supported estimated-amount completion rule and the complete E04 messages.",
        "Approve EUR 125.00, link the later EUR 29.12 advice through the original reference, and replace the hold with the new local total.",
        "Replay the advice to verify the hold is not changed twice. Confirm the same flow classification and replacement rule for any enabled taxi or other estimated-amount use case."
      ],
      "expected_result": "Keep both authorization records and their link. advice is true on the later message. Hold becomes EUR 29.12 and EUR 95.88 is released once. Posted movement stays EUR 0.00: this example contains no presentment.",
      "guide": "/guides/authorization-flow#authorization-amount-update-eur-12500-to-eur-2912",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    },
    {
      "test_id": "T26",
      "name": "Additional authorization amount update",
      "applies_when": "Programs that support this additional-authorization flow",
      "fixture_cases": [
        "E03"
      ],
      "input_kind": "Recorded masked webhook sequence",
      "steps": [
        "Configure this fixture's increment rule.",
        "Approve EUR 28.94, apply the additional EUR 0.40 once, then clear EUR 29.34."
      ],
      "expected_result": "Hold becomes EUR 29.34 and clearing posts EUR -29.34 once. This additional authorization has advice false; the completion advice in T25 has advice true. A declined additional authorization does not cancel the earlier approval.",
      "guide": "/guides/authorization-flow#additional-authorization-eur-2894--eur-040",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    },
    {
      "test_id": "T27",
      "name": "Excess amounts, funding and reconciliation",
      "applies_when": "Every integration",
      "fixture_cases": [
        "E06"
      ],
      "input_kind": "Recorded masked webhook sequence",
      "steps": [
        "Measure the cleared amount not covered by an eligible hold.",
        "Apply the configured funding policy and reconcile through the enabled ledger access."
      ],
      "expected_result": "The uncovered amount is accounted for under the agreed funding rules. Reconciliation retains distinct presentment IDs. A higher clearing amount alone does not establish a chargeback right.",
      "guide": "/guides/clearing-and-reconciliation#amount-differences-funding-and-disputes",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    },
    {
      "test_id": "T28",
      "name": "Authorization HTTP response contract",
      "applies_when": "Integrator-managed decisions",
      "fixture_cases": [
        "E01",
        "E08"
      ],
      "input_kind": "Test harness scenario",
      "steps": [
        "Send approval and decline requests through the actual local HTTP receiver.",
        "Assert HTTP 200 with status success for approval and HTTP 403 with status error for decline; HTTP 200 with status error fails this test.",
        "Repeat the same initial request and verify both the saved HTTP status and body.",
        "Deliver the declined outcome confirmation and verify durable-receipt acknowledgement without a new purchase decision."
      ],
      "expected_result": "The transport status and JSON decision agree. Approval is 200/success; decline is 403/error. Retries replay both. A received decline confirmation is acknowledged with 200/success and creates no posting.",
      "guide": "/guides/authorization-flow#return-the-decision-with-the-correct-http-status",
      "status": "pending",
      "evidence": [],
      "not_applicable_reason": ""
    }
  ]
}
