écrits/tutorial/2026/10
● Tutorial30 oct. 2026·26 min

Surveiller le seuil d'immatriculation TVA saoudien en TypeScript

Construisez le contrôle de fin de mois que le règlement d'application de la TVA saoudienne impose à toute entreprise non immatriculée : le test des douze mois écoulés (article 3), l'estimation des douze mois à venir (article 4), tous les CR additionnés, les actifs immobilisés exclus, le délai de 30 jours et les dates d'effet, l'article 9(1) pour les ventes au taux zéro, et la radiation de l'article 13 telle que modifiée en 2024. Testé, en TypeScript.

En Arabie saoudite, une entreprise ne devient pas redevable de l'immatriculation à la TVA à une date que quelqu'un inscrit dans un agenda. Elle le devient à une fin de mois, la première où un calcul dépasse 375 000 SAR, et c'est un calcul que presque personne ne fait. L'article 3 du règlement d'application de la TVA impose à toute personne résidente non immatriculée de calculer, à la fin de chaque mois, la valeur de ses livraisons sur les douze mois écoulés. L'article 4 ajoute un second calcul à la même fin de mois : une estimation des livraisons attendues sur les douze mois suivants. Le dépassement de l'un ou de l'autre ouvre un délai de 30 jours, et le manquer coûte 10 000 SAR d'amende au titre de l'article 41 de la loi, avant le coût plus lourd de la TVA due sur des factures émises sans elle.

Nous avons expliqué les règles aux dirigeants dans Immatriculation à la TVA saoudienne : le test des 375 000 SAR, mois par mois. Ce tutoriel s'adresse à ceux qui construisent les systèmes qu'ils utilisent : logiciels comptables, ERP, back-offices de caisse et cabinets de tenue de comptes qui suivent des dizaines de petites entreprises. Il transforme la « routine mensuelle » de cet article en un petit moteur TypeScript testé, à exécuter à chaque clôture mensuelle.

Ce que vous allez construire

Un module sans dépendance qui, à partir du registre des ventes et des charges d'une personne et d'une prévision sur douze mois, répond à quatre questions à chaque fin de mois :

  1. L'immatriculation est-elle obligatoire maintenant ? L'article 3 sur les douze mois écoulés, l'article 4 sur les douze suivants, avec tous les registres de commerce (CR) de la personne additionnés et les cessions d'actifs immobilisés exclues.
  2. Avant quand, et à partir de quand ? Le dernier jour pour déposer la demande (30 jours après la fin du mois) et la date d'effet fixée par chaque article, qui ne sont pas les mêmes.
  3. Est-elle seulement facultative ? L'article 7, à 187 500 SAR, sur les livraisons ou les charges, et l'article 9(1) pour un vendeur dont toutes les livraisons sont au taux zéro.
  4. Une fois immatriculée, la personne doit-elle ou peut-elle se faire radier ? Les paragraphes (1), (3), (6) et (8) de l'article 13, avec la condition ajoutée par la résolution 24-06-01 en 2024.

Il refuse aussi de répondre quand il le doit : un mois sans aucune écriture, ou une prévision absente, donne unknown, jamais « sous le seuil ».

Prérequis

  • Node.js 20 ou plus récent
  • Une pratique de TypeScript
  • Savoir comment votre système stocke les lignes de vente et de charges par mois
  • Aucun accès API à la ZATCA n'est nécessaire. Rien ici n'appelle la ZATCA ; c'est le calcul qui décide si vous devez vous immatriculer

Une note sur les sources. Les numéros d'articles et les citations proviennent du texte arabe consolidé actuel du règlement d'application publié par la ZATCA, qui intègre la résolution du conseil 24-06-01 du 19 novembre 2024. Le PDF anglais de la ZATCA est une édition plus ancienne et ne contient pas cette résolution. Lorsque les deux diffèrent, le texte arabe fait foi. Chaque point où le moteur retient une lecture que le texte ne tranche pas figure dans la section « Les lectures retenues par ce moteur » plus bas.

Étape 1 : Mise en place du projet

mkdir vat-threshold && cd vat-threshold
npm init -y
npm install --save-dev typescript tsx @types/node
mkdir src tests

tsconfig.json

{
  "compilerOptions": {
    "strict": true,
    "target": "ES2022",
    "module": "commonjs",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "noEmit": true,
    "types": ["node"]
  },
  "include": ["src", "tests", "cli.ts"]
}

Chaque montant est un nombre entier de halalas. Un test de seuil qui compare des riyals en virgule flottante finira par décider que 374 999,99 plus 0,01 est inférieur à 375 000. Les entiers font de « exactement au seuil » une valeur testable.

Étape 2 : Les mois et les deux règles de date

Le règlement compte en mois civils et utilise deux règles de date, toutes deux à l'article 3 :

  • Le délai. « Dans les trente jours suivant la fin de ce mois. » La fin de novembre est le 30 novembre, donc le dernier jour est le 30 décembre. La fin de février 2027 est le 28 février, donc le dernier jour est le 30 mars.
  • La date d'effet de l'article 3. « À compter du début du mois suivant le mois au cours duquel la demande d'immatriculation a été déposée. » Elle dépend de la date à laquelle la personne dépose réellement, pas de celle du dépassement.

src/months.ts

// Calendar months as "YYYY-MM" strings, and the two date rules the
// Regulations use: "within thirty days of the end of that month" and
// "from the start of the month following".
 
export type MonthKey = string; // "2026-11"
 
const MONTH_RE = /^(\d{4})-(0[1-9]|1[0-2])$/;
 
export function parseMonth(m: MonthKey): { year: number; month: number } {
  const match = MONTH_RE.exec(m);
  if (!match) throw new Error(`Bad month key: ${m}`);
  return { year: Number(match[1]), month: Number(match[2]) };
}
 
export function addMonths(m: MonthKey, n: number): MonthKey {
  const { year, month } = parseMonth(m);
  const index = year * 12 + (month - 1) + n;
  const y = Math.floor(index / 12);
  const mo = (index % 12) + 1;
  return `${y}-${String(mo).padStart(2, "0")}`;
}
 
/** The n months ending with `last`, oldest first. */
export function windowEnding(last: MonthKey, n: number): MonthKey[] {
  return Array.from({ length: n }, (_, i) => addMonths(last, i - n + 1));
}
 
/** The n months starting the month after `m`. */
export function windowAfter(m: MonthKey, n: number): MonthKey[] {
  return Array.from({ length: n }, (_, i) => addMonths(m, i + 1));
}
 
/** Last calendar day of the month, as "YYYY-MM-DD". */
export function monthEnd(m: MonthKey): string {
  const { year, month } = parseMonth(m);
  const d = new Date(Date.UTC(year, month, 0));
  return d.toISOString().slice(0, 10);
}
 
/** First day of the month, as "YYYY-MM-DD". */
export function monthStart(m: MonthKey): string {
  return `${m}-01`;
}
 
export function addDays(isoDate: string, days: number): string {
  const d = new Date(`${isoDate}T00:00:00Z`);
  d.setUTCDate(d.getUTCDate() + days);
  return d.toISOString().slice(0, 10);
}
 
export function monthOf(isoDate: string): MonthKey {
  return isoDate.slice(0, 7);
}

Les mois sont des chaînes comme "2026-11", parce que c'est ainsi qu'une période comptable clôturée est généralement identifiée, et parce que la comparaison de chaînes au format YYYY-MM les trie correctement. Les dates sont calculées en UTC pour que le fuseau horaire du serveur ne puisse pas décaler un délai d'un jour.

Étape 3 : Le registre, une personne, tous ses CR

L'entrée est le registre dont vous disposez déjà : lignes de vente et de charges par mois. Trois règles décident de ce qui entre dans le chiffre comparé au seuil.

Tous les CR ensemble. L'immatriculation est attachée à la personne, pas au registre de commerce. Dans la décision VD-224-2020, la première chambre des infractions et litiges de TVA de Dammam a jugé que l'immatriculation à la TVA est liée à tous les CR d'une personne. Le propriétaire de deux entreprises individuelles, chacune sous le seuil, peut être au-dessus. Le registre porte donc un champ cr pour l'audit, et les totaux l'ignorent.

Les actifs immobilisés sont exclus. L'article 6(5) exclut la livraison d'un actif immobilisé du total des livraisons annuelles, à condition que l'actif ait servi à exploiter l'activité sous un CR ou une licence et n'ait pas été détenu pour en tirer des loyers ou pour être revendu. Vendre la camionnette de livraison ne vous fait pas passer le seuil. Vendre un appartement que vous louiez n'est pas couvert par cette exclusion : enregistrez-le sous sa vraie nature.

Les livraisons exonérées sont un paramètre, pas une constante. Les articles 3 et 4 disent que la valeur se calcule « conformément aux exigences prévues par l'Accord ». La plupart des guides professionnels excluent les livraisons exonérées du calcul, mais le règlement ne le dit pas dans ces articles. Le moteur prend donc la règle en paramètre et, à l'étape 5, calcule les deux.

src/register.ts

// The monthly register: every sale and expense line, tagged with the
// commercial registration (CR) it was booked under and what kind of supply
// it is. Amounts are integer halalas.
import { MonthKey } from "./months";
 
export type Halalas = number;
 
export type SupplyKind =
  | "standard"       // 15%
  | "zero"           // zero-rated (exports, qualifying medicines...)
  | "exempt"         // residential rent, some financial services
  | "capital_asset"  // sale of an asset used in the business (Art. 6(5))
  | "out_of_scope";  // not a supply in the Kingdom
 
export interface SupplyLine {
  month: MonthKey;
  cr: string;
  kind: SupplyKind;
  amount: Halalas;
}
 
export interface ExpenseLine {
  month: MonthKey;
  cr: string;
  amount: Halalas;
}
 
export type ExemptPolicy = "exclude" | "include";
 
export interface MonthTotals {
  /** Supplies that count toward the threshold under the chosen policy. */
  counted: Halalas;
  /** Of which zero-rated. */
  zeroRated: Halalas;
  expenses: Halalas;
}
 
/**
 * Adds every CR together: registration attaches to the person, not to
 * the commercial registration (GSTC VD-224-2020). Capital asset sales and
 * out-of-scope lines never count. Exempt lines count only under "include".
 */
export function totalsByMonth(
  supplies: SupplyLine[],
  expenses: ExpenseLine[],
  exemptPolicy: ExemptPolicy,
): Map<MonthKey, MonthTotals> {
  const out = new Map<MonthKey, MonthTotals>();
  const row = (m: MonthKey) => {
    let r = out.get(m);
    if (!r) {
      r = { counted: 0, zeroRated: 0, expenses: 0 };
      out.set(m, r);
    }
    return r;
  };
 
  for (const s of supplies) {
    if (!Number.isInteger(s.amount)) throw new Error(`Non-integer halalas on ${s.month}`);
    const r = row(s.month);
    if (s.kind === "capital_asset" || s.kind === "out_of_scope") continue;
    if (s.kind === "exempt" && exemptPolicy === "exclude") continue;
    r.counted += s.amount;
    if (s.kind === "zero") r.zeroRated += s.amount;
  }
  for (const e of expenses) {
    if (!Number.isInteger(e.amount)) throw new Error(`Non-integer halalas on ${e.month}`);
    row(e.month).expenses += e.amount;
  }
  return out;
}

Le kind attribué à chaque ligne est la classification que votre système fait déjà pour la facturation. Si votre fichier articles ne distingue pas une cession d'immobilisation d'une vente de stock, corrigez cela d'abord : le moniteur ne vaut que ce que vaut l'étiquette.

Étape 4 : Le test de fin de mois

C'est le cœur. À une fin de mois asOf, le moteur additionne les douze mois se terminant par asOf (article 3) et les douze mois prévus à partir du mois suivant (article 4), puis classe.

Deux comparaisons se ressemblent et ne sont pas identiques :

  • Obligatoire : « dépasse ». L'article 3 emploie « تجاوزت », dépasse. Exactement 375 000,00 SAR ne dépasse pas 375 000 ; 375 000,01 le dépasse. Le code utilise une comparaison stricte.
  • Facultatif : « n'est pas inférieur à ». L'article 7 emploie « لا يقل عن », pas inférieur à. Exactement 187 500 SAR suffit. Le code utilise une comparaison inclusive, et l'article 7 compte les livraisons ou les charges, ce qui compte pour une entreprise qui dépense en aménagement avant de vendre quoi que ce soit.

Les dates d'effet diffèrent aussi selon l'article :

  • Article 3(2) : le début du mois suivant le mois du dépôt de la demande. Le moteur indique la date applicable si la personne dépose le dernier jour, au plus tard.
  • Article 4(2) : « à compter du début du premier mois au cours duquel ses livraisons annuelles dans le Royaume devaient dépasser » le seuil. Le moteur indique le début du premier mois prévu, la lecture la plus précoce. La TVA est due à partir de cette date, qu'elle ait été facturée ou non.

L'article 9(1) est l'exception pour une personne au-dessus du seuil dont toutes les livraisons sont au taux zéro, comme un exportateur qui vend toute sa production à l'étranger. Elle n'est pas tenue de s'immatriculer et peut le faire volontairement. Un seul riyal au taux normal dans la période suffit pour la perdre : le test est donc une égalité exacte entre le total au taux zéro et le total compté.

src/registration.ts

// The month-end registration test: Implementing Regulations Art. 3
// (trailing twelve months), Art. 4 (next twelve months), Art. 7
// (voluntary), Art. 9(1) (zero-rated only).
import {
  MonthKey, addDays, addMonths, monthEnd, monthOf, monthStart,
  windowAfter, windowEnding,
} from "./months";
import {
  ExemptPolicy, ExpenseLine, Halalas, MonthTotals, SupplyLine, totalsByMonth,
} from "./register";
 
export const MANDATORY_THRESHOLD: Halalas = 375_000_00;
export const VOLUNTARY_THRESHOLD: Halalas = 187_500_00;
export const DAYS_TO_APPLY = 30;
 
export interface ForecastMonth {
  month: MonthKey;
  supplies: Halalas;   // standard + zero-rated; no capital assets, no exempt
  zeroRated: Halalas;  // of which zero-rated
  exempt: Halalas;     // counted only under exemptPolicy "include"
  expenses: Halalas;
}
 
export interface Ledger {
  firstTradingMonth: MonthKey; // months before it count as zero
  supplies: SupplyLine[];
  expenses: ExpenseLine[];
  exemptPolicy: ExemptPolicy;
}
 
export type Status =
  | "mandatory"          // apply within 30 days
  | "zero_rated_only"    // Art. 9(1): over the line, but excepted
  | "voluntary_eligible" // Art. 7: may apply
  | "below"
  | "unknown";           // data missing; no conclusion
 
export type Trigger = "ART3_TRAILING" | "ART4_FORWARD";
 
export interface Assessment {
  asOf: MonthKey;
  trailing: Halalas;
  trailingExpenses: Halalas;
  forward: Halalas | null;
  forwardExpenses: Halalas | null;
  status: Status;
  triggers: Trigger[];
  applyBy: string | null;
  /** Art. 3(2): start of the month after the filing month, if filed on applyBy. */
  art3EffectiveIfFiledOnDeadline: string | null;
  /** Art. 4(2), earliest reading: the first month of the forecast window. */
  art4EffectiveFrom: string | null;
  issues: string[];
}
 
/** Art. 3(2): registration starts the month after the application month. */
export function art3EffectiveFrom(filedOn: string): string {
  return monthStart(addMonths(monthOf(filedOn), 1));
}
 
export function assessMonth(
  asOf: MonthKey,
  ledger: Ledger,
  forecast: ForecastMonth[] | null,
): Assessment {
  const issues: string[] = [];
  const totals = totalsByMonth(ledger.supplies, ledger.expenses, ledger.exemptPolicy);
  const zero: MonthTotals = { counted: 0, zeroRated: 0, expenses: 0 };
 
  // Trailing twelve months, all CRs together.
  let trailing = 0, trailingZero = 0, trailingExpenses = 0;
  for (const m of windowEnding(asOf, 12)) {
    const t = totals.get(m);
    if (!t && m >= ledger.firstTradingMonth) issues.push(`MISSING_MONTH ${m}`);
    const r = t ?? zero;
    trailing += r.counted;
    trailingZero += r.zeroRated;
    trailingExpenses += r.expenses;
  }
 
  // Next twelve months, as estimated at this month-end.
  let forward: Halalas | null = null, forwardZero = 0;
  let forwardExpenses: Halalas | null = null;
  const expected = windowAfter(asOf, 12);
  if (!forecast) {
    issues.push("NO_FORECAST");
  } else if (
    forecast.length !== 12 ||
    forecast.some((f, i) => f.month !== expected[i])
  ) {
    issues.push("FORECAST_NOT_NEXT_12_MONTHS");
  } else {
    forward = 0; forwardExpenses = 0;
    for (const f of forecast) {
      forward += f.supplies + (ledger.exemptPolicy === "include" ? f.exempt : 0);
      forwardZero += f.zeroRated;
      forwardExpenses += f.expenses;
    }
  }
 
  // "Exceeds": strictly more than 375,000.
  const triggers: Trigger[] = [];
  if (trailing > MANDATORY_THRESHOLD) triggers.push("ART3_TRAILING");
  if (forward !== null && forward > MANDATORY_THRESHOLD) triggers.push("ART4_FORWARD");
 
  const applyBy = addDays(monthEnd(asOf), DAYS_TO_APPLY);
  const base = { asOf, trailing, trailingExpenses, forward, forwardExpenses, issues };
 
  if (triggers.length > 0) {
    // Art. 9(1): every supply in the window that crossed is zero-rated.
    const allZero =
      (!triggers.includes("ART3_TRAILING") || trailingZero === trailing) &&
      (!triggers.includes("ART4_FORWARD") || forwardZero === forward);
    if (allZero) {
      return {
        ...base, status: "zero_rated_only", triggers, applyBy: null,
        art3EffectiveIfFiledOnDeadline: null, art4EffectiveFrom: null,
      };
    }
    return {
      ...base, status: "mandatory", triggers, applyBy,
      art3EffectiveIfFiledOnDeadline: triggers.includes("ART3_TRAILING")
        ? art3EffectiveFrom(applyBy) : null,
      art4EffectiveFrom: triggers.includes("ART4_FORWARD")
        ? monthStart(expected[0]) : null,
    };
  }
 
  const none = {
    triggers, applyBy: null,
    art3EffectiveIfFiledOnDeadline: null, art4EffectiveFrom: null,
  };
 
  // Art. 7: "not less than" 187,500, supplies OR expenses, past OR next.
  const voluntary =
    trailing >= VOLUNTARY_THRESHOLD ||
    trailingExpenses >= VOLUNTARY_THRESHOLD ||
    (forward ?? 0) >= VOLUNTARY_THRESHOLD ||
    (forwardExpenses ?? 0) >= VOLUNTARY_THRESHOLD;
 
  // A gap in the data can hide a crossing. Never call it "below".
  if (issues.length > 0) return { ...base, ...none, status: "unknown" };
  return { ...base, ...none, status: voluntary ? "voluntary_eligible" : "below" };
}

Observez ce qui se passe quand les données sont incomplètes. Un mois compris entre le premier mois d'activité et asOf sans aucune ligne est signalé MISSING_MONTH, car un mois sans ligne est le plus souvent un mois que personne n'a clôturé, pas un mois sans ventes. Un mois réellement calme doit porter une ligne explicite à zéro. Une prévision absente ou mal alignée est signalée aussi. Dans les deux cas, le moteur signale quand même un dépassement sur les douze mois écoulés, qui est déjà un fait, mais il ne dira ni below ni voluntary_eligible, car les données manquantes pourraient cacher un dépassement.

Étape 5 : Calculer des deux façons

Comme la question des exonérations n'est pas tranchée dans les articles qui posent le test, la sortie honnête est les deux réponses et un signal lorsqu'elles diffèrent. Un bailleur qui exploite aussi un commerce est le cas typique : le loyer d'habitation est exonéré, et l'ajouter change le total.

Ajoutez ceci à src/registration.ts :

src/registration.ts

/**
 * The Regulations do not say, in Arts. 3 and 4, whether exempt supplies
 * count. Run both and flag the months where the answer depends on it.
 */
export function assessBothWays(
  asOf: MonthKey,
  ledger: Ledger,
  forecast: ForecastMonth[] | null,
) {
  const exclude = assessMonth(asOf, { ...ledger, exemptPolicy: "exclude" }, forecast);
  const include = assessMonth(asOf, { ...ledger, exemptPolicy: "include" }, forecast);
  return { exclude, include, dependsOnExempt: exclude.status !== include.status };
}

Quand dependsOnExempt est vrai, le système ne doit pas décider seul. Il doit transmettre le mois à une personne, avec les deux chiffres, pour un avis écrit. L'article de blog dit la même chose aux dirigeants ; voici la version code de cette phrase.

Étape 6 : La radiation selon l'article 13

Une fois la personne immatriculée, les mêmes chiffres mensuels répondent à une autre question. L'article 13 sépare ce qui est obligatoire de ce qui est permis :

  • 13(1) : l'activité a cessé. La personne doit se faire radier et déposer la demande dans les 30 jours (13(4)). La résolution 24-06-01 a ajouté que toutes les obligations fiscales doivent être réglées avant l'approbation de la demande, et que la cession d'activité qui met entièrement fin à l'activité est incluse.
  • 13(3) : obligatoire, à toute fin de mois, lorsque les trois conditions sont réunies. Les livraisons ou les charges des douze derniers mois ne dépassent pas 187 500 SAR ; celles des vingt-quatre derniers mois ne dépassent pas 375 000 SAR ; et elles ne devraient pas dépasser 187 500 SAR sur les mois à venir.
  • 13(6) : permise lorsque les livraisons des douze derniers mois ne dépassent pas 375 000 SAR et ne devraient pas le dépasser sur les douze prochains.
  • 13(8) : verrouillée. Une personne immatriculée volontairement au titre de l'article 7 et toujours en activité ne peut pas demander sa radiation avant douze mois. La cessation d'activité est l'exception.

src/deregistration.ts

// Deregistration under Implementing Regulations Art. 13, as amended by
// ZATCA Board resolution 24-06-01 (19 November 2024).
import {
  MonthKey, addDays, monthEnd, windowAfter, windowEnding,
} from "./months";
import { Halalas, totalsByMonth } from "./register";
import {
  DAYS_TO_APPLY, ForecastMonth, Ledger, MANDATORY_THRESHOLD, VOLUNTARY_THRESHOLD,
} from "./registration";
 
export interface Registration {
  effectiveFrom: string;   // "YYYY-MM-DD"
  voluntary: boolean;      // registered under Art. 7
}
 
export type DeregStatus =
  | "must_deregister"  // Art. 13(1) or 13(3): apply within 30 days
  | "may_deregister"   // Art. 13(6)
  | "locked"           // Art. 13(8): voluntary, under twelve months
  | "stay"
  | "unknown";
 
export interface DeregAssessment {
  asOf: MonthKey;
  status: DeregStatus;
  rule: string | null;
  applyBy: string | null;
  lockedUntil: string | null;
  blockers: string[];
  issues: string[];
}
 
export interface DeregInput {
  asOf: MonthKey;
  ledger: Ledger;
  registration: Registration;
  forecast: ForecastMonth[] | null;
  ceasedOn?: string;               // date the economic activity stopped
  obligationsOutstanding: boolean; // unfiled returns, unpaid balances
}
 
function oneYearAfter(isoDate: string): string {
  const next = `${Number(isoDate.slice(0, 4)) + 1}${isoDate.slice(4)}`;
  return next.endsWith("-02-29") ? next.replace("-02-29", "-03-01") : next;
}
 
export function assessDeregistration(input: DeregInput): DeregAssessment {
  const { asOf, ledger, registration, forecast } = input;
  const issues: string[] = [];
  // Resolution 24-06-01: obligations settled before approval.
  const blockers = input.obligationsOutstanding ? ["OBLIGATIONS_OUTSTANDING"] : [];
  const base = { asOf, blockers, issues, lockedUntil: null as string | null };
 
  // 13(1): activity stopped. 13(8) does not lock this case.
  if (input.ceasedOn) {
    return {
      ...base, status: "must_deregister", rule: "13(1)",
      applyBy: addDays(input.ceasedOn, DAYS_TO_APPLY),
    };
  }
 
  const totals = totalsByMonth(ledger.supplies, ledger.expenses, ledger.exemptPolicy);
  const sum = (months: MonthKey[]) => {
    let supplies = 0, expenses = 0;
    for (const m of months) {
      const t = totals.get(m);
      if (!t && m >= ledger.firstTradingMonth) issues.push(`MISSING_MONTH ${m}`);
      supplies += t?.counted ?? 0;
      expenses += t?.expenses ?? 0;
    }
    return { supplies, expenses };
  };
  const last12 = sum(windowEnding(asOf, 12));
  const last24 = sum(windowEnding(asOf, 24));
 
  let next: { supplies: Halalas; expenses: Halalas } | null = null;
  const expected = windowAfter(asOf, 12);
  if (!forecast) issues.push("NO_FORECAST");
  else if (forecast.length !== 12 || forecast.some((f, i) => f.month !== expected[i])) {
    issues.push("FORECAST_NOT_NEXT_12_MONTHS");
  } else {
    next = { supplies: 0, expenses: 0 };
    for (const f of forecast) {
      next.supplies += f.supplies + (ledger.exemptPolicy === "include" ? f.exempt : 0);
      next.expenses += f.expenses;
    }
  }
 
  if (issues.length > 0) {
    return { ...base, issues: [...new Set(issues)], status: "unknown", rule: null, applyBy: null };
  }
  const n = next!;
 
  // 13(8): a voluntary registrant still trading cannot apply in year one.
  const lockedUntil = oneYearAfter(registration.effectiveFrom);
  if (registration.voluntary && monthEnd(asOf) < lockedUntil) {
    return { ...base, lockedUntil, status: "locked", rule: "13(8)", applyBy: null };
  }
 
  // 13(3): all three, supplies and expenses alike ("does not exceed").
  const must =
    last12.supplies <= VOLUNTARY_THRESHOLD && last12.expenses <= VOLUNTARY_THRESHOLD &&
    last24.supplies <= MANDATORY_THRESHOLD && last24.expenses <= MANDATORY_THRESHOLD &&
    n.supplies <= VOLUNTARY_THRESHOLD && n.expenses <= VOLUNTARY_THRESHOLD;
  if (must) {
    return {
      ...base, status: "must_deregister", rule: "13(3)",
      applyBy: addDays(monthEnd(asOf), DAYS_TO_APPLY),
    };
  }
 
  // 13(6): supplies only, against the mandatory threshold.
  if (last12.supplies <= MANDATORY_THRESHOLD && n.supplies <= MANDATORY_THRESHOLD) {
    return { ...base, status: "may_deregister", rule: "13(6)", applyBy: null };
  }
  return { ...base, status: "stay", rule: null, applyBy: null };
}

Les obligations non réglées sont renvoyées comme un obstacle, pas comme un statut. La personne peut rester tenue de se faire radier ; la demande ne sera pas approuvée tant que l'obstacle subsiste. Séparer les deux permet au système d'afficher la bonne tâche : « déposer la déclaration manquante, puis demander la radiation », et non « rien à faire ».

Étape 7 : Des tests qui fixent les règles

Chaque règle ci-dessus a son test, et chaque test utilise des chiffres vérifiables à la main. Les tests d'immatriculation reprennent le commerce de l'article de blog, ouvert en janvier.

tests/registration.test.ts

import { test } from "node:test";
import assert from "node:assert/strict";
import { addMonths, windowAfter } from "../src/months";
import { SupplyLine } from "../src/register";
import {
  ForecastMonth, Ledger, art3EffectiveFrom, assessBothWays, assessMonth,
} from "../src/registration";
 
const SAR = (n: number) => Math.round(n * 100);
 
// The shop from the article: opened January, sales in SAR per month.
const SHOP: [string, number][] = [
  ["2026-01", 20_000], ["2026-02", 22_000], ["2026-03", 25_000],
  ["2026-04", 28_000], ["2026-05", 30_000], ["2026-06", 25_000],
  ["2026-07", 40_000], ["2026-08", 45_000], ["2026-09", 50_000],
  ["2026-10", 60_000], ["2026-11", 55_000],
];
 
function shop(extra: SupplyLine[] = []): Ledger {
  return {
    firstTradingMonth: "2026-01",
    supplies: [
      ...SHOP.map(([month, sar]) => ({ month, cr: "1010000001", kind: "standard" as const, amount: SAR(sar) })),
      ...extra,
    ],
    expenses: [],
    exemptPolicy: "exclude",
  };
}
 
function flat(asOf: string, sarPerMonth: number, zero = 0): ForecastMonth[] {
  return windowAfter(asOf, 12).map((month) => ({
    month, supplies: SAR(sarPerMonth), zeroRated: SAR(zero), exempt: 0, expenses: 0,
  }));
}
 
test("Art. 3: trailing total crosses at the November month-end", () => {
  const oct = assessMonth("2026-10", shop(), flat("2026-10", 20_000));
  assert.equal(oct.trailing, SAR(345_000));
  assert.equal(oct.status, "voluntary_eligible");
 
  const nov = assessMonth("2026-11", shop(), flat("2026-11", 20_000));
  assert.equal(nov.trailing, SAR(400_000));
  assert.equal(nov.status, "mandatory");
  assert.deepEqual(nov.triggers, ["ART3_TRAILING"]);
  assert.equal(nov.applyBy, "2026-12-30");
  assert.equal(nov.art3EffectiveIfFiledOnDeadline, "2027-01-01");
});
 
test("Art. 3(2): effective date follows the filing month", () => {
  assert.equal(art3EffectiveFrom("2026-12-05"), "2027-01-01");
  assert.equal(art3EffectiveFrom("2027-01-02"), "2027-02-01");
});
 
test("exactly 375,000 does not exceed; 375,000.01 does", () => {
  const at = (sar: number) => assessMonth("2026-12", {
    firstTradingMonth: "2026-12",
    supplies: [{ month: "2026-12", cr: "A", kind: "standard", amount: SAR(sar) }],
    expenses: [], exemptPolicy: "exclude",
  }, flat("2026-12", 0));
  assert.equal(at(375_000).status, "voluntary_eligible");
  assert.equal(at(375_000.01).status, "mandatory");
});
 
test("Art. 7: exactly 187,500 in expenses alone is enough to volunteer", () => {
  const a = assessMonth("2026-12", {
    firstTradingMonth: "2026-12", supplies: [],
    expenses: [{ month: "2026-12", cr: "A", amount: SAR(187_500) }],
    exemptPolicy: "exclude",
  }, flat("2026-12", 0));
  assert.equal(a.status, "voluntary_eligible");
});
 
test("Art. 4: a signed contract crosses before the invoices do", () => {
  const ledger: Ledger = {
    firstTradingMonth: "2026-06",
    supplies: ["2026-06", "2026-07", "2026-08", "2026-09"].map((month) => ({
      month, cr: "A", kind: "standard" as const, amount: SAR(5_000),
    })),
    expenses: [], exemptPolicy: "exclude",
  };
  const a = assessMonth("2026-09", ledger, flat("2026-09", 50_000));
  assert.equal(a.trailing, SAR(20_000));
  assert.equal(a.forward, SAR(600_000));
  assert.deepEqual(a.triggers, ["ART4_FORWARD"]);
  assert.equal(a.applyBy, "2026-10-30");
  assert.equal(a.art4EffectiveFrom, "2026-10-01");
  assert.equal(a.art3EffectiveIfFiledOnDeadline, null);
});
 
test("capital asset sales never count (Art. 6(5))", () => {
  const a = assessMonth("2026-10", shop([
    { month: "2026-10", cr: "1010000001", kind: "capital_asset", amount: SAR(90_000) },
  ]), flat("2026-10", 20_000));
  assert.equal(a.trailing, SAR(345_000));
  assert.equal(a.status, "voluntary_eligible");
});
 
test("two CRs, one person: totals are added (VD-224-2020)", () => {
  const ledger: Ledger = {
    firstTradingMonth: "2026-01",
    supplies: Array.from({ length: 12 }, (_, i) => addMonths("2026-01", i)).flatMap((month) => [
      { month, cr: "CR-1", kind: "standard" as const, amount: SAR(20_000) },
      { month, cr: "CR-2", kind: "standard" as const, amount: SAR(15_000) },
    ]),
    expenses: [], exemptPolicy: "exclude",
  };
  const a = assessMonth("2026-12", ledger, flat("2026-12", 0));
  assert.equal(a.trailing, SAR(420_000));
  assert.equal(a.status, "mandatory");
});
 
test("Art. 9(1): over the line but all zero-rated is excepted", () => {
  const ledger: Ledger = {
    firstTradingMonth: "2026-12",
    supplies: [{ month: "2026-12", cr: "A", kind: "zero", amount: SAR(400_000) }],
    expenses: [], exemptPolicy: "exclude",
  };
  assert.equal(assessMonth("2026-12", ledger, flat("2026-12", 0)).status, "zero_rated_only");
 
  ledger.supplies.push({ month: "2026-12", cr: "A", kind: "standard", amount: SAR(1) });
  assert.equal(assessMonth("2026-12", ledger, flat("2026-12", 0)).status, "mandatory");
});
 
test("a month with no lines is a gap, not a zero", () => {
  const ledger = shop();
  ledger.supplies = ledger.supplies.filter((s) => s.month !== "2026-08");
  const a = assessMonth("2026-10", ledger, flat("2026-10", 20_000));
  assert.equal(a.status, "unknown");
  assert.deepEqual(a.issues, ["MISSING_MONTH 2026-08"]);
});
 
test("no forecast: never 'below', but a trailing crossing still stands", () => {
  assert.equal(assessMonth("2026-10", shop(), null).status, "unknown");
  assert.equal(assessMonth("2026-11", shop(), null).status, "mandatory");
});
 
test("a forecast that starts in the wrong month is rejected", () => {
  const a = assessMonth("2026-10", shop(), flat("2026-09", 0));
  assert.deepEqual(a.issues, ["FORECAST_NOT_NEXT_12_MONTHS"]);
});
 
test("exempt rent decides the answer: flagged", () => {
  const r = assessBothWays("2026-10", shop([
    { month: "2026-10", cr: "1010000001", kind: "exempt", amount: SAR(40_000) },
  ]), flat("2026-10", 20_000));
  assert.equal(r.exclude.status, "voluntary_eligible");
  assert.equal(r.include.status, "mandatory");
  assert.equal(r.dependsOnExempt, true);
});

tests/deregistration.test.ts

import { test } from "node:test";
import assert from "node:assert/strict";
import { addMonths, windowAfter } from "../src/months";
import { ForecastMonth, Ledger } from "../src/registration";
import { DeregInput, assessDeregistration } from "../src/deregistration";
 
const SAR = (n: number) => Math.round(n * 100);
 
// Twenty-four months of flat sales, January 2025 to December 2026.
function ledger(sarPerMonth: number): Ledger {
  return {
    firstTradingMonth: "2025-01",
    supplies: Array.from({ length: 24 }, (_, i) => ({
      month: addMonths("2025-01", i), cr: "A", kind: "standard" as const, amount: SAR(sarPerMonth),
    })),
    expenses: [], exemptPolicy: "exclude",
  };
}
 
function flat(asOf: string, sarPerMonth: number): ForecastMonth[] {
  return windowAfter(asOf, 12).map((month) => ({
    month, supplies: SAR(sarPerMonth), zeroRated: 0, exempt: 0, expenses: 0,
  }));
}
 
function input(sarPerMonth: number, forecastSar: number, over: Partial<DeregInput> = {}): DeregInput {
  return {
    asOf: "2026-12",
    ledger: ledger(sarPerMonth),
    registration: { effectiveFrom: "2025-01-01", voluntary: false },
    forecast: flat("2026-12", forecastSar),
    obligationsOutstanding: false,
    ...over,
  };
}
 
test("13(3): all three conditions met, must apply within 30 days", () => {
  // 12 x 15,000 = 180,000; 24 x 15,000 = 360,000; next 12 = 120,000.
  const a = assessDeregistration(input(15_000, 10_000));
  assert.equal(a.status, "must_deregister");
  assert.equal(a.rule, "13(3)");
  assert.equal(a.applyBy, "2027-01-30");
});
 
test("13(3)(b) fails on the 24-month total: only 13(6) applies", () => {
  // 24 x 16,000 = 384,000 exceeds 375,000.
  const a = assessDeregistration(input(16_000, 10_000));
  assert.equal(a.status, "may_deregister");
  assert.equal(a.rule, "13(6)");
});
 
test("13(6): below 375,000 both ways, deregistration is optional", () => {
  const a = assessDeregistration(input(30_000, 30_000));
  assert.equal(a.status, "may_deregister");
});
 
test("over 375,000 in the next twelve months: stay registered", () => {
  const a = assessDeregistration(input(30_000, 32_000));
  assert.equal(a.status, "stay");
});
 
test("13(8): a voluntary registrant still trading is locked for twelve months", () => {
  const a = assessDeregistration(input(5_000, 5_000, {
    registration: { effectiveFrom: "2026-03-01", voluntary: true },
  }));
  assert.equal(a.status, "locked");
  assert.equal(a.lockedUntil, "2027-03-01");
});
 
test("13(1): stopping the activity overrides the lock", () => {
  const a = assessDeregistration(input(5_000, 0, {
    registration: { effectiveFrom: "2026-03-01", voluntary: true },
    ceasedOn: "2026-12-10",
  }));
  assert.equal(a.status, "must_deregister");
  assert.equal(a.applyBy, "2027-01-09");
});
 
test("24-06-01: outstanding obligations are a blocker, not a status", () => {
  const a = assessDeregistration(input(15_000, 10_000, { obligationsOutstanding: true }));
  assert.equal(a.status, "must_deregister");
  assert.deepEqual(a.blockers, ["OBLIGATIONS_OUTSTANDING"]);
});
 
test("a gap in the 24 months gives no answer", () => {
  const i = input(15_000, 10_000);
  i.ledger.supplies = i.ledger.supplies.filter((s) => s.month !== "2025-04");
  const a = assessDeregistration(i);
  assert.equal(a.status, "unknown");
  assert.deepEqual(a.issues, ["MISSING_MONTH 2025-04"]);
});

Lancez-les :

npx tsx --test tests/*.test.ts

Les 20 tests doivent passer. Avant de faire confiance à une telle suite, cassez le code exprès et vérifiez qu'un test échoue. Nous l'avons fait pour neuf modifications : comparaison obligatoire rendue inclusive, comparaison facultative rendue stricte, 31 jours au lieu de 30, cessions d'immobilisations comptées, date de l'article 4 décalée d'un mois, article 9(1) ignorant les ventes au taux normal, fenêtre de 24 mois ramenée à 12, verrou de l'article 13(8) supprimé, mois manquants ignorés. Chacune a fait échouer au moins un test.

Étape 8 : Le rapport de fin de mois

Une petite CLI lit un fichier de registre et affiche une ligne par fin de mois. Le script ci-dessous construit le registre du commerce, avec une prévision du propriétaire fixée à chaque fin de mois à 25 000 SAR par mois.

make-demo.mjs

const shop = [20000,22000,25000,28000,30000,25000,40000,45000,50000,60000,55000];
const months = shop.map((_, i) => `2026-${String(i+1).padStart(2,"0")}`);
const add = (m, n) => { const [y, mo] = m.split("-").map(Number); const i = y*12 + mo-1 + n; return `${Math.floor(i/12)}-${String(i%12+1).padStart(2,"0")}`; };
const forecasts = {};
for (const m of months.slice(5)) forecasts[m] = Array.from({length:12}, (_, i) => ({ month: add(m, i+1), supplies: 2500000, zeroRated: 0, exempt: 0, expenses: 0 }));
const ledger = { firstTradingMonth: "2026-01", exemptPolicy: "exclude", expenses: [],
  supplies: months.map((month, i) => ({ month, cr: "1010000001", kind: "standard", amount: shop[i]*100 })) };
console.log(JSON.stringify({ ledger, months: months.slice(5), forecasts }, null, 2));

cli.ts

// Month-end report for one person. Usage: npx tsx cli.ts register.json
import { readFileSync } from "node:fs";
import { assessBothWays, ForecastMonth, Ledger } from "./src/registration";
 
interface RegisterFile {
  ledger: Ledger;
  months: string[];
  forecasts: Record<string, ForecastMonth[]>;
}
 
const file: RegisterFile = JSON.parse(readFileSync(process.argv[2], "utf8"));
const sar = (h: number | null) =>
  h === null ? "-" : (h / 100).toLocaleString("en-US", { minimumFractionDigits: 2 });
 
for (const asOf of file.months) {
  const r = assessBothWays(asOf, file.ledger, file.forecasts[asOf] ?? null);
  const a = r.exclude;
  const parts = [
    asOf.padEnd(8),
    sar(a.trailing).padStart(12),
    sar(a.forward).padStart(12),
    a.status.padEnd(18),
  ];
  if (a.applyBy) parts.push(`apply by ${a.applyBy}`);
  if (a.art3EffectiveIfFiledOnDeadline) {
    parts.push(`registered from ${a.art3EffectiveIfFiledOnDeadline} if filed that day`);
  }
  if (a.art4EffectiveFrom) parts.push(`VAT due from ${a.art4EffectiveFrom}`);
  if (r.dependsOnExempt) parts.push(`(with exempt: ${r.include.status})`);
  if (a.issues.length) parts.push(a.issues.join(", "));
  console.log(parts.join("  "));
}
node make-demo.mjs > register.json
npx tsx cli.ts register.json
2026-06     150,000.00    300,000.00  voluntary_eligible
2026-07     190,000.00    300,000.00  voluntary_eligible
2026-08     235,000.00    300,000.00  voluntary_eligible
2026-09     285,000.00    300,000.00  voluntary_eligible
2026-10     345,000.00    300,000.00  voluntary_eligible
2026-11     400,000.00    300,000.00  mandatory           apply by 2026-12-30  registered from 2027-01-01 if filed that day

Le total des douze mois écoulés dépasse le seuil à la fin de novembre, le dernier jour pour déposer est le 30 décembre, et un dépôt ce jour-là donne une immatriculation au 1er janvier 2027. C'est le tableau de l'article de blog, produit désormais par le code.

Changez maintenant une seule donnée : supposons qu'à la fin de juillet l'estimation honnête du propriétaire était « à peu près comme juillet, chaque mois », soit 40 000 SAR par mois. Passez la prévision de juillet à 4000000 halalas par mois et relancez. La ligne de juillet devient :

2026-07     190,000.00    480,000.00  mandatory           apply by 2026-08-30  VAT due from 2026-08-01

Le test prospectif a dépassé le seuil quatre mois avant le test rétrospectif et, selon la lecture la plus précoce de l'article 4(2), la TVA est due à partir du 1er août. Pour une entreprise en croissance, c'est le cas normal et non l'exception : la prévision dépasse généralement en premier. Le compte d'assistance officiel de la ZATCA a indiqué publiquement que, pour les nouvelles entreprises, le seuil se calcule sur les ventes attendues des douze mois à venir. Un moniteur qui ne fait que sommer le passé contrôle le test qui arrive en second.

Étape 9 : L'intégrer dans un vrai système

Le moteur est volontairement petit. Le travail qui le rend utile se fait autour.

Lancez-le à la clôture mensuelle, pas selon son propre calendrier. Le chiffre rétrospectif n'a de sens qu'une fois le mois clôturé. Déclenchez l'évaluation sur l'événement de clôture, pour chaque client ou entité non immatriculé, et conservez le résultat.

Conservez la prévision avec le résultat. Chaque évaluation est écrite une fois, avec la prévision qui l'a produite, et jamais modifiée. Si la ZATCA immatricule plus tard la personne d'office au titre de l'article 6(1), la date d'effet est celle que fixent les articles 3 et 4. Votre trace de ce qui était attendu, et quand, prouve ce que la personne savait à chaque fin de mois.

Alimentez la prévision avec ce qui existe. Contrats signés, abonnements récurrents et bons de commande confirmés valent mieux qu'un pourcentage de croissance. Quand un contrat est signé en cours de mois, la prévision de cette fin de mois doit l'inclure. C'est exactement le cas pour lequel l'article 4 a été écrit.

Alertez avant l'échéance, pas le jour même. Créez une tâche le jour où le statut passe à mandatory, un rappel dix jours avant applyBy et une escalade deux jours avant. Transmettez unknown à la personne qui clôture les comptes, avec la liste des mois manquants.

Intégrez la décision de prix à la tâche. L'immatriculation change les prix. Les contrats conclus « TTC » verront la TVA prélevée dessus : 15 riyals sur 115 encaissés deviennent de l'impôt. La tâche qui dit « déposer avant le 30 décembre » doit aussi demander « ajouter 15 % ou l'absorber ? ». Le calculateur de TVA montre les deux sens pour un prix donné.

Après l'immatriculation, passez le relais. Une personne immatriculée a besoin d'un autre moteur : la déclaration périodique. Nous l'avons construit dans le tutoriel du moteur de déclaration de TVA saoudienne. Dès la date d'effet, vos clients vérifieront aussi votre numéro avant de déduire la TVA que vous leur facturez ; le vérificateur de numéro de TVA saoudien montre ce qu'ils voient, et le tutoriel du contrôle du numéro de TVA fournisseur construit ce contrôle de leur côté.

Les lectures retenues par ce moteur

Là où le texte ne tranche pas, le moteur retient une lecture et le dit. Modifiez-les si votre conseil lit autrement.

  • Date d'effet de l'article 4(2). « Le début du premier mois au cours duquel les livraisons annuelles devaient dépasser » le seuil. Nous la lisons comme le premier mois de la fenêtre de prévision, la date la plus précoce, qui correspond aux réponses publiques de la ZATCA sur les nouvelles entreprises. Une lecture plus tardive est possible : le premier mois où un total glissant incluant la prévision dépasserait le seuil. C'est sur la date la plus précoce qu'il faut planifier.
  • Livraisons exonérées. Non tranché aux articles 3 et 4. Le moteur calcule les deux et signale les mois où la réponse en dépend.
  • Article 13(3), « livraisons ou charges ne dépassant pas ». Nous exigeons que les deux restent sous la limite. Si l'une dépasse, la personne reste éligible à l'immatriculation volontaire, et l'obligation de sortir ne naît pas.
  • Fenêtre de l'article 13(3)(c). L'arabe dit « l'un quelconque des onze mois suivants », l'anglais « ce mois et les onze suivants ». Le moteur utilise la même prévision de douze mois que pour l'article 4, qui couvre les deux.
  • La condition de règlement de la résolution 24-06-01. La résolution l'a inscrite au paragraphe 1, le cas de cessation d'activité, avec les mots « dans tous les cas ». Le moteur signale les obligations non réglées dans tous les cas de radiation par précaution, puisque toute radiation exige l'approbation de la ZATCA.
  • Non modélisé. Les articles 6(2) et 6(3) permettent à la ZATCA, sur demande, de reporter une date d'effet (au plus tard au début du mois suivant le dépôt de la demande) ou de l'avancer. L'article 9(2) lui permet de regrouper des personnes liées par notification ; si vous en recevez une, mettez les lignes des deux personnes dans un seul registre. Les non-résidents (article 5) suivent une tout autre règle.

Dépannage

Le statut est unknown alors que chaque mois a des ventes. Vérifiez les clés de mois. Une ligne indexée "2026-8" au lieu de "2026-08" ne correspond à aucun mois de la fenêtre, et son mois apparaît manquant. Normalisez les clés à l'import.

Le total rétrospectif diffère de la balance. La balance inclut les cessions d'immobilisations, les produits hors champ et, selon votre règle, les produits exonérés. Comparez avec counted, pas avec le chiffre d'affaires.

Un client avec deux CR a deux résultats séparés. Vous lancez le moteur par CR. Lancez-le par personne, avec les lignes de tous ses CR dans un seul registre. L'immatriculation est attachée à la personne.

La prévision est rejetée avec FORECAST_NOT_NEXT_12_MONTHS. Elle doit commencer le mois suivant asOf et couvrir exactement douze mois. Une prévision bâtie sur l'année civile ne sera plus alignée après janvier.

Le moteur dit mandatory pour un exportateur. Au moins une ligne de la période n'est pas marquée zero. L'article 9(1) exige que toutes les livraisons soient au taux zéro. Une seule vente locale, même petite, fait tomber l'exception.

Un immatriculé volontaire apparaît locked après une mauvaise année. L'article 13(8) bloque la demande pendant douze mois à compter de la date d'effet tant que la personne exerce son activité. La ZATCA peut toujours la radier au titre du 13(5) ; la personne, elle, ne peut pas en faire la demande.

Pour aller plus loin

Conclusion

Le seuil d'immatriculation n'est pas un événement annuel. C'est un test à chaque fin de mois, mené deux fois, une sur ce qui s'est passé et une sur ce qui est attendu, sur des chiffres qui incluent tous les CR de la personne et excluent les cessions d'actifs immobilisés. Le moteur de ce tutoriel le fait en quelques centaines de lignes, refuse de deviner quand les données manquent, donne le délai et la date d'effet applicables, et couvre la sortie prévue à l'article 13 tel que modifié en 2024. La difficulté n'est pas le code. C'est l'étiquetage de chaque ligne et une prévision honnête, et les deux ont leur place dans la clôture mensuelle.

Vous développez un logiciel comptable ou un ERP pour des entreprises saoudiennes, ou vous tenez les comptes de plusieurs d'entre elles ? Nous intégrons ce type de contrôle dans les systèmes existants : le registre, l'alimentation de la prévision, le déclencheur de fin de mois et les alertes. Envoyez-nous la façon dont votre système stocke aujourd'hui ventes et contrats, et nous vous répondrons sur l'endroit où le moniteur s'insère et ce dont il a besoin. Contactez-nous.