TL;DR

MonSuiviCA keeps its URSSAF contribution and income tax calculations in a single TypeScript module. URSSAF is the French agency that collects social contributions. The module's package has 199 tests, 99 of them in the file dedicated to this module. On November 1, 2026, they all pass.

Every figure in this article is the one in the code as of November 1, 2026. These figures describe software and do not constitute tax advice.

The product's origin story is in the MonSuiviCA launch article.

One module holds the numbers

The file packages/shared/src/calculations.ts serves both the app and the marketing site. Three constants give it its structure:

  • TAUX_VERSION: version 2026-02, the effective date, and retrievedAt, the date the rates were last checked (2026-08-06).
  • TAUX: mainland rates, flat-rate allowances, VAT thresholds and regime ceilings, namely €83,600 for services and €203,100 for sales.
  • TAUX_ZONES: rates by tax zone and by sector.

Here is what TAUX_ZONES contains, leaving out tourist rentals. SSI is the social security scheme for the self-employed, CIPAV is the pension fund for some liberal professions, and DROM means the French overseas departments and regions:

Zone Services Sales Liberal SSI Liberal CIPAV
Mainland France 21.2% 12.3% 25.6% 23.2%
DROM, under 2 years 3.6% 2.1% 4.3% 7.8%
DROM, 2 to 3 years 10.6% 6.2% 12.8% 11.6%
DROM, 4 years and more 14.2% 8.2% 17.1% 15.5%
Mayotte 9.1% 5.3% 12% 16.6%

This centralization answers a real failure. One marketing page displayed a €77,700 ceiling in its text and €83,600 in the result of the calculator just below it.

Since August 31, 2026, pages read their amounts from bareme.ts, a display layer on top of the shared module. A test fails, naming the file and line, if an old value comes back in their code.

Resolving the right rate

The function getEffectiveUrssafRates picks an account's rates in this order:

  1. Custom rates entered in the settings.
  2. Rates derived from the zone, the pension fund and ACRE, a French aid for starting or taking over a business.
  3. null, which makes the calculations fall back on the mainland rates.

ACRE comes down to a dated multiplier. It expresses the share of the rate that is kept, and the code picks it from the start date entered:

export function getAcreMultiplier(refDate: Date = new Date()): number { return refDate < new Date('2026-07-01') ? 0.50 : 0.75 } 

With a start date of September 1, 2026, the mainland services rate therefore goes from 21.2% to 15.9%.

Counting only what was collected

The entire contribution base goes through isEncaisse. An invoice with the attente (pending) status leaves the contribution and income tax calculations.

The displayed contribution equals the rate times the amount collected over the declared period, a month or a calendar quarter, with no annual smoothing. For the mixed sector, getRevenueRate applies the rate entry by entry: sales on one side, services on the other.

Calculating tax at household level

Taxable income starts from revenue, minus a flat allowance of 50% for services, 71% for sales and 34% for liberal professions. The scale then applies bracket by bracket:

export function calculateIR(taxableIncome: number): number { let total = 0 for (const b of IR_BRACKETS) { if (taxableIncome <= b.from) break total += (Math.min(taxableIncome, b.to) - b.from) * b.rate } return total } 

IR_BRACKETS holds five brackets, at 0, 11, 30, 41 and 45%. This isolated calculation gets a household wrong. Twenty thousand euros of taxable income weigh differently depending on the household's tax shares and the spouse's salary.

So I wrote calculateIRFoyer. It returns an additional cost: the household's tax with the activity, minus the household's tax without it. For €20,000 of taxable income and 2 tax shares, the code gives:

  • With no other income: €0.
  • With €60,000 of other income: €6,000, which is €10,207.98 minus €4,207.98.

A custom rate keeps priority, then comes the household, then the scale alone. The family quotient cap, the low-income discount, tax reductions and tax credits stay outside the model. The function's comment says so, and calls the result an indicative estimate.

Three levels of checks

CI runs the shared package's tests in a dedicated step. They check three things:

  • Values: each zone, and ACRE on both sides of July 1, 2026.
  • Properties: the additional cost equals the difference between the two taxes, never goes negative, and the household counts at least 1 share.
  • A lock: bareme.test.ts pins the ceilings, VAT thresholds and rates checked on service-public.gouv.fr on August 31, 2026. Changing the scale forces a change to the test as well.

This value test covers the €20,000 case on the scale alone:

it('taxes 11% between 11600 and 29579', () => { // (20000 - 11600) * 0.11 expect(calculateIR(20000)).toBeCloseTo(924, 2) }) 

A script completes the setup when the web app builds. check-taux-urssaf.js warns when retrievedAt is older than 6 months, or as soon as a January 1 or July 1 has passed since that date. It does not stop the build.

What these tests don't prove

No accounting audit is proven as of November 1, 2026. The chartered accountant validation list in docs/business/formules-comptables.md has no box checked, and the document still carries a draft status.

The public calculator shows its own limit under its result, in French: « Estimation indicative. Impôt calculé sur 1 part fiscale, sans déductions. » In English: an indicative estimate, with tax calculated on 1 tax share and no deductions.