DEV Community

Muhammad Shahid
Muhammad Shahid

Posted on

A "3% fee" that's really 27% APR — I built a vanilla JS calculator to prove it

A friend who runs a small delivery business got a factoring quote last month: "just a 3% fee on your invoices." He was ready to sign — his credit card charges more than that monthly.

I asked him one question: 3% compared to what?

His bank had offered a business credit line at 9% APR. The factoring company quoted 3%. Those numbers aren't in the same unit — one is charged once per invoice, the other is charged per year. So I did what developers do: I built a calculator to convert one into the other.

The result: his "3%" was actually a 27% APR. This post is the math behind it and the vanilla JS that implements it.

The unit trap

Quick context: invoice factoring means selling your unpaid invoices to a company (a "factor") for immediate cash. They advance you most of the invoice today — typically 90% — keep a fee, and collect the full amount from your customer later.

The trap is that a factoring fee is a flat percentage of one invoice, charged once. To compare it with any loan or credit line, you have to annualize it:

Effective APR = (total fees ÷ cash advanced) × (365 ÷ days until paid)
Enter fullscreen mode Exit fullscreen mode

Two details in that formula do all the damage:

  1. Divide by cash advanced, not invoice face value. The factor only gives you ~90% of the invoice, but charges the fee on 100% of it. You're paying for money you never touched.
  2. Divide by the days your customer actually pays — not the stated terms. Net-30 customers pay on day 45.

Worked example — $50,000 invoice, 3% fee, 90% advance, paid in 45 days:

  • Fee: $1,500
  • Cash advanced: $45,000
  • $1,500 ÷ $45,000 = 0.0333
  • × (365 ÷ 45) = 27.0% APR

And here's the perverse part: the faster your customers pay, the higher the APR climbs. The same 3% fee settled in 30 days is ~41% APR. The fee buys a fixed amount of time — less time makes the annual rate balloon.

The core function

/**
 * Convert a factoring quote into an effective APR.
 * All percentages are plain numbers (3 means 3%).
 */
function effectiveAPR({ invoice, feePct, advancePct, daysToPay, otherFees = 0 }) {
  if (invoice <= 0 || daysToPay <= 0 || advancePct <= 0) {
    throw new Error("Invoice, advance %, and days must all be positive");
  }

  const fee = invoice * (feePct / 100) + otherFees;
  const cashAdvanced = invoice * (advancePct / 100);

  const costRatio = fee / cashAdvanced;
  const apr = costRatio * (365 / daysToPay);

  return {
    fee,
    cashAdvanced,
    costRatio,        // e.g. 0.0333
    apr,              // e.g. 0.2704
  };
}

const quote = effectiveAPR({
  invoice: 50000,
  feePct: 3,
  advancePct: 90,
  daysToPay: 45,
});

console.log((quote.apr * 100).toFixed(1) + "% APR"); // "27.0% APR"
Enter fullscreen mode Exit fullscreen mode

Note the guard clause — a daysToPay of 0 doesn't throw a division error in JS, it silently returns Infinity, and that's worse. Fail loudly.

Money formatting without floating-point surprises

0.1 + 0.2 !== 0.3 is a meme until it's on an invoice. For display, don't hand-roll rounding — use Intl.NumberFormat:

const usd = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
  maximumFractionDigits: 0, // quotes are approximate anyway
});

usd.format(quote.fee);          // "$1,500"
usd.format(quote.cashAdvanced); // "$45,000"
Enter fullscreen mode Exit fullscreen mode

(If you're doing serious currency math, move to integer cents before computing. For a quote estimator, formatting at the display layer is enough.)

Wiring up a minimal form

<form id="quote-form">
  <label>Invoice amount <input type="number" id="invoice" value="50000" /></label>
  <label>Factoring fee % <input type="number" id="fee" value="3" step="0.1" /></label>
  <label>Advance rate % <input type="number" id="advance" value="90" /></label>
  <label>Days until customer pays <input type="number" id="days" value="45" /></label>
  <button type="submit">Calculate true APR</button>
</form>
<p id="result"></p>
Enter fullscreen mode Exit fullscreen mode
const $ = (id) => document.getElementById(id);

$("quote-form").addEventListener("submit", (e) => {
  e.preventDefault();

  try {
    const { apr, fee, cashAdvanced } = effectiveAPR({
      invoice: parseFloat($("invoice").value),
      feePct: parseFloat($("fee").value),
      advancePct: parseFloat($("advance").value),
      daysToPay: parseFloat($("days").value),
    });

    $("result").textContent =
      `${usd.format(fee)} fee on ${usd.format(cashAdvanced)} advanced ` +
      `= ${(apr * 100).toFixed(1)}% effective APR`;
  } catch (err) {
    $("result").textContent = err.message;
  }
});
Enter fullscreen mode Exit fullscreen mode

That's the whole engine — no framework, no dependencies, works in a <script> tag.

The sneaky part: per-period fees

Just when you understand flat fees: many contracts don't charge one fee. They charge a rate per 15- or 30-day period, with part periods rounded up in full. Your customer paying one day into a new period costs you the entire period.

Implementing that rounding is a one-liner with Math.ceil:

function perPeriodAPR({ invoice, ratePerPeriodPct, periodDays, daysToPay, advancePct }) {
  const periods = Math.ceil(daysToPay / periodDays); // day 31 of a 30-day period = 2 periods
  const fee = invoice * (ratePerPeriodPct / 100) * periods;
  const cashAdvanced = invoice * (advancePct / 100);

  return (fee / cashAdvanced) * (365 / daysToPay);
}

// 1.5% per 15 days, customer pays in 30 days:
console.log((perPeriodAPR({ invoice: 50000, ratePerPeriodPct: 1.5, periodDays: 15, daysToPay: 30, advancePct: 90 }) * 100).toFixed(1));
// 40.6% APR

// Same deal, customer pays one day later (day 31):
console.log((perPeriodAPR({ invoice: 50000, ratePerPeriodPct: 1.5, periodDays: 15, daysToPay: 31, advancePct: 90 }) * 100).toFixed(1));
// 58.9% APR
Enter fullscreen mode Exit fullscreen mode

One day late: +18 points of APR, because Math.ceil rounded up to a whole extra period and the shorter denominator pushed the annualized rate up. Contract writers know exactly what they're doing.

The comparison that changed my friend's mind

The whole point of an APR is comparing against your alternative. A 9% credit line covering the same $45,000 gap for 45 days costs:

const creditLineCost = 45000 * 0.09 * (45 / 365);
usd.format(creditLineCost); // "$499"
Enter fullscreen mode Exit fullscreen mode

$499 vs $1,500 for the same money over the same period. Factoring still wins in plenty of situations — it's often the only facility a young business can qualify for, because the factor underwrites your customers' credit instead of yours. But you should choose it knowing the price, not reading a rate card designed to obscure it.

Try it live

I packaged all of this — flat and per-period fee structures, an advance/fees/reserve breakdown, and a factoring-vs-credit-line-vs-waiting comparison — into a free tool, no signup:

👉 Invoice Factoring Calculator — true APR, fees & reserve

It also covers how factoring rates vary by industry (trucking runs 2–4%, retail up to 8% — and annualized, that's 27%–70%+ APR).

If you're building your own version, the two functions above are the entire math core — the rest is form handling and nicer charts. The same annualization logic works for any "fee per transaction" product: merchant cash advances, payday-style short bridges, early-payment discounts. If a price isn't quoted in APR, make it a habit to convert it.

Happy to share the reserve breakdown logic or a React version if there's interest — drop a comment.

Top comments (0)