Back to ER Diagram
Three-Way Match

Three-Way Match Logic

Complete documentation of PO-GRN-Invoice matching process, variance handling, tolerance thresholds, and automatic reconciliation for accounts payable.

6 Tables Involved
Schema v2.3
Logistics Module

What is Three-Way Match?

Three-way matching is a critical accounts payable control that compares three documents before authorizing payment: the Purchase Order (what was ordered), the Goods Receipt Note (what was received), and the Vendor Invoice (what is being billed).

Purchase Order

What was ordered

GRN

What was received

Invoice

What is billed

Why Three-Way Match?

Prevents payment for goods not ordered, not received, or at incorrect prices. Reduces fraud, billing errors, and overpayments.

Tables Involved

po.purchase_orders + po.po_items

  • Source of truth for ordered quantities and agreed prices
  • Unit price, quantity, and line total per item
  • PO must be approved before receiving goods

logistics.grns + logistics.grn_items

  • Records actual goods received at warehouse
  • Accepted quantity, rejected quantity, inspection status
  • Links to PO items via po_item_id

logistics.vendor_invoices + logistics.invoice_items

  • Vendor's billing document
  • Invoiced quantity, unit price, tax, total
  • Links to both PO items and GRN items

logistics.three_way_match

  • Records the matching result for each PO-GRN-Invoice combination
  • match_status: MATCHED, PARTIAL_MATCH, MISMATCH
  • variance_amount, variance_reason for discrepancies

Matching Criteria

For a successful match, the following must align across all three documents:

Criteria PO GRN Invoice Tolerance
Quantity Ordered Qty Accepted Qty Invoiced Qty 0% (exact match)
Unit Price Agreed Price N/A Billed Price +/- 2%
Line Total PO Line Amount N/A Invoice Line Amount +/- 2%
Tax Amount Estimated Tax N/A Actual Tax +/- 1%
Total Amount PO Total N/A Invoice Total +/- 2%

Tolerance Configuration

Tolerance thresholds are configurable in config.system_settings with keys like MATCH_PRICE_TOLERANCE_PCT and MATCH_QTY_TOLERANCE_PCT.

Variance Example

Example matching scenario for "Steel Rods - 100 units @ $50 each":

Quantity
100
PO Ordered
98
GRN Received
98
Invoiced
Match
Unit Price
$50.00
PO Price
-
N/A
$50.50
Invoiced
1% (OK)
Line Total
$5,000
PO Total
$4,900
GRN Value
$4,949
Invoiced
Match

Match Statuses

Status Condition Action Required
MATCHED All criteria within tolerance Auto-approve for payment
PARTIAL_MATCH Some items match, others have minor variance Review and manually approve
MISMATCH Significant variance beyond tolerance Investigate, create debit note if needed
PENDING GRN or Invoice not yet received Wait for documents

Matching Process Steps

1

Invoice Received

When a vendor invoice is entered into the system, it links to the PO and optionally to specific GRNs.

INSERT INTO logistics.vendor_invoices
    (invoice_number, po_id, vendor_id, grn_id, invoice_date, total_amount, status)
VALUES
    ('INV-2026-0456', @poId, @vendorId, @grnId, NOW(), 4949.00, 'PENDING_MATCH');
2

Trigger Matching Job

The matching process can be triggered automatically on invoice entry or via scheduled batch job.

public async Task PerformThreeWayMatchAsync(long invoiceId)
{
    var invoice = await GetInvoiceWithItemsAsync(invoiceId);
    var po = await GetPOWithItemsAsync(invoice.PoId);
    var grn = await GetGRNWithItemsAsync(invoice.GrnId);

    var matchResult = CompareDocuments(po, grn, invoice);

    await SaveMatchResultAsync(matchResult);
}
3

Compare Line Items

Each invoice line is compared against corresponding PO and GRN lines.

SELECT
    poi.id AS po_item_id,
    poi.quantity AS po_qty,
    poi.unit_price AS po_price,
    gi.accepted_qty AS grn_qty,
    ii.quantity AS inv_qty,
    ii.unit_price AS inv_price,
    ABS(poi.unit_price - ii.unit_price) / poi.unit_price * 100 AS price_variance_pct,
    CASE
        WHEN gi.accepted_qty = ii.quantity
             AND ABS(poi.unit_price - ii.unit_price) / poi.unit_price <= 0.02
        THEN 'MATCHED'
        ELSE 'MISMATCH'
    END AS line_status
FROM po.po_items poi
JOIN logistics.grn_items gi ON gi.po_item_id = poi.id
JOIN logistics.invoice_items ii ON ii.po_item_id = poi.id
WHERE ii.invoice_id = @invoiceId;
4

Calculate Overall Status

Aggregate line-level results to determine overall match status.

private MatchStatus CalculateOverallStatus(List<LineMatchResult> lineResults)
{
    if (lineResults.All(l => l.Status == "MATCHED"))
        return MatchStatus.Matched;

    if (lineResults.Any(l => l.Status == "MISMATCH")
        && lineResults.Sum(l => l.VarianceAmount) > toleranceAmount)
        return MatchStatus.Mismatch;

    return MatchStatus.PartialMatch;
}
5

Save Match Record

Store the matching result for audit and workflow processing.

INSERT INTO logistics.three_way_match
    (po_id, grn_id, invoice_id, match_status, variance_amount, variance_reason, matched_at)
VALUES
    (@poId, @grnId, @invoiceId,
     'MATCHED',
     49.00,
     'Price variance 1% within tolerance',
     NOW());
6

Update Invoice Status

Based on match result, update invoice status to proceed with approval or flag for review.

UPDATE logistics.vendor_invoices
SET status = CASE
    WHEN @matchStatus = 'MATCHED' THEN 'APPROVED_FOR_PAYMENT'
    WHEN @matchStatus = 'PARTIAL_MATCH' THEN 'PENDING_REVIEW'
    ELSE 'DISPUTED'
END
WHERE id = @invoiceId;

Handling Mismatches

Quantity Mismatch

Invoice > GRN: Create a debit note for unbilled quantity. Only pay for what was received.

Price Mismatch

Invoice Price > PO Price: Escalate for approval or reject. Document in variance_reason.

Missing GRN

Invoice received before goods? Mark as PENDING until GRN is created. Never pay before receiving.

-- Create debit note for over-invoiced quantity
INSERT INTO logistics.debit_notes
    (debit_note_number, vendor_id, grn_id, reason, amount, status, created_by)
VALUES
    (GenerateDebitNoteNumber(), @vendorId, @grnId,
     'Invoice quantity 100 exceeds received quantity 98',
     100.00,  -- 2 units @ $50
     'PENDING',
     @userId);

Configuration Settings

Setting Key Default Value Description
MATCH_PRICE_TOLERANCE_PCT 2.0 Price variance tolerance percentage
MATCH_QTY_TOLERANCE_PCT 0.0 Quantity must match exactly
MATCH_TAX_TOLERANCE_PCT 1.0 Tax calculation variance tolerance
MATCH_AUTO_APPROVE true Auto-approve matched invoices for payment
MATCH_REQUIRE_GRN true Require GRN before invoice can be matched