Build guide · About 30 minutes · Rated High on the map

Never pay the same invoice twice

Duplicate payments are not a fraud problem in most portfolios. They are a filing problem. A vendor emails an invoice, hears nothing for three weeks, and emails it again with a new number. Both get coded. Both get paid. Nobody notices, because the two payments are three weeks and forty invoices apart.

Why the accounting system does not catch it

Most property accounting software will stop you entering the same vendor and the same invoice number twice. That catches the honest duplicate and nothing else. The expensive version is the same work billed under a different number: a re-issue, a statement paid alongside the individual invoices it summarizes, or a work order billed once by the vendor and once by the property manager who fronted it.

None of those trip an exact-match rule, because nothing about them is exactly the same except the amount and roughly the date. Which is the whole basis for catching them.

Recovering a duplicate payment ninety days later means asking a vendor you still need to send money back. Catching it before approval costs nothing and nobody has to be told.

What you need

The build

  1. Normalize the vendor name first Every duplicate check fails on vendor names before it fails on anything else. The same company is in your system four ways because four people typed it.
    function normVendor(name) {
      return name
        .toLowerCase()
        .replace(/[.,'&-]/g, " ")
        .replace(/\b(inc|llc|l l c|corp|co|company|ltd|the)\b/g, " ")
        .replace(/\s+/g, " ")
        .trim();
    }
    // "Southeast Roofing, LLC" and "SE Roofing LLC" still differ.
    // Normalizing is step one; the amount match below is what carries the check.
  2. Score the match instead of demanding one An exact rule catches the easy case and misses the costly one. Score each candidate and let the score decide what happens.
    function score(a, b) {
      let s = 0;
      if (normVendor(a.vendor) === normVendor(b.vendor)) s += 40;
      if (Math.abs(a.amount - b.amount) < 0.01)         s += 40;
      else if (Math.abs(a.amount - b.amount) / b.amount < 0.02) s += 20;
    
      const gap = Math.abs(new Date(a.date) - new Date(b.date)) / 86400000;
      if (gap <= 7)       s += 15;
      else if (gap <= 45) s += 10;
      else if (gap <= 120) s += 5;
    
      if (a.invoiceNo && b.invoiceNo) {
        const x = String(a.invoiceNo).replace(/\D/g, "");
        const y = String(b.invoiceNo).replace(/\D/g, "");
        if (x === y) s += 20;                       // same number, different prefix
      }
      return s;
    }
  3. Suppress the vendors that are supposed to repeat Landscaping bills $850 on the first of every month. Pest control bills $310 every quarter. These will score 80 against last month forever, and after the fourth false alarm somebody turns the automation off, which is worse than not having it. Maintain a short list of contract vendors and require a tighter date window for them.
    const RECURRING = new Set(["greenscape lawn", "orkin", "waste management"]);
    // for these, only flag when the gap is under 20 days
    const tooSoon = RECURRING.has(normVendor(a.vendor)) && gapDays < 20;
  4. Hold for review, never reject Anything at 80 or above goes to a queue with both invoices side by side and a one-line reason. A person spends fifteen seconds on it. Blocking payment automatically will eventually stop a legitimate invoice, and the cost of a late fee and an annoyed vendor is higher than the cost of fifteen seconds.

The thresholds, and why these ones

ScoreWhat it usually meansWhat happens
95 and aboveSame vendor, same amount, within a week. Almost always a genuine re-send.Hold and notify. These are the ones that pay for the build.
80 to 94Same vendor and amount, further apart, or amounts within 2%. Often a re-issue under a new number.Hold for review with both invoices shown.
60 to 79Two of the three signals. Frequently legitimate.Log it, do not interrupt anybody. Review the log monthly and use it to tune.
Under 60Noise.Ignore.

Start at 80 and watch what it holds for a month. If more than one in four holds turns out to be legitimate, the problem is almost always a recurring vendor missing from the suppression list rather than the threshold being wrong.

Three things that will break it

Statements paid alongside invoices. A vendor sends individual invoices during the month and a statement at the end that totals them. Somebody pays the statement, somebody else pays the invoices. The statement amount matches nothing individually, so a pairwise check never sees it. Flag any invoice whose amount equals the sum of two or more open items from the same vendor in the same period, and treat a document containing the word statement as needing a human by default.

Split and partial billing. A $12,000 roof job billed as $6,000 deposit and $6,000 on completion will score 80 and it is entirely legitimate. Both halves are the same vendor and the same amount, usually within your window. Look for deposit, progress, draw or final in the description and lower the score when you find one, rather than letting the reviewer see the same false positive on every capital project.

Credit memos read as invoices. A negative amount that gets imported without its sign becomes a positive charge that matches the original perfectly. Check for a negative amount or a credit indicator before scoring, and route credits somewhere else entirely. This one is quiet and it moves the wrong direction, so it rarely surfaces on its own.

If you would rather not build it

The kit is the finished version of everything above: the scenario blueprint, the vendor normalizer, the scoring function, the review queue, and the thresholds as a spreadsheet you can edit. Email [email protected] if you would rather have it built into what you already run.

Get the next build guide

One email when a new guide goes up. Nothing else.

No sequence, no upsell drip. Unsubscribe link on every send.