لا تصبح المنشأة في السعودية ملزمة بالتسجيل في ضريبة القيمة المضافة في تاريخ يكتبه أحد في التقويم. تصبح ملزمة في نهاية شهر، أول نهاية شهر يتجاوز فيها حساب معيّن 375,000 ريال، وهو حساب لا يكاد أحد يجريه. توجب المادة الثالثة من اللائحة التنفيذية على كل شخص مقيم غير مسجل أن يحسب في نهاية كل شهر قيمة توريداته خلال الاثني عشر شهرًا السابقة. وتضيف المادة الرابعة حسابًا ثانيًا في نهاية الشهر نفسه: تقديرًا لقيمة التوريدات المتوقعة خلال الاثني عشر شهرًا اللاحقة. تجاوز أيٍّ منهما للحد يبدأ مهلة ثلاثين يومًا، وتفويتها يكلّف غرامة 10,000 ريال بموجب المادة الحادية والأربعين من النظام، قبل الكلفة الأكبر: ضريبة مستحقة على فواتير صدرت بدونها.
شرحنا القواعد لأصحاب المنشآت في التسجيل في ضريبة القيمة المضافة: اختبار 375,000 ريال شهرًا بشهر. هذا الدرس موجّه لمن يبنون الأنظمة التي يستخدمها هؤلاء: برامج المحاسبة، وأنظمة تخطيط الموارد، والأنظمة الخلفية لنقاط البيع، ومكاتب مسك الدفاتر التي تخدم عشرات المنشآت الصغيرة. نحوّل فيه "الروتين الشهري" الوارد في ذلك المقال إلى محرك صغير مختبَر بلغة TypeScript يعمل عند كل إقفال شهري.
ما الذي ستبنيه
وحدة بلا اعتماديات خارجية، تأخذ سجل المبيعات والمصاريف لشخص واحد وتقديرًا للاثني عشر شهرًا القادمة، وتجيب في نهاية كل شهر عن أربعة أسئلة:
- هل التسجيل إلزامي الآن؟ المادة الثالثة على الاثني عشر شهرًا السابقة، والمادة الرابعة على الاثني عشر اللاحقة، مع جمع كل السجلات التجارية التي يملكها الشخص واستبعاد بيع الأصول الرأسمالية.
- حتى متى، ومن متى؟ آخر يوم لتقديم الطلب (ثلاثون يومًا بعد نهاية الشهر) وتاريخ النفاذ الذي تحدده كل مادة، وهما ليسا التاريخ نفسه.
- هل هو اختياري فقط؟ المادة السابعة عند 187,500 ريال، على التوريدات أو المصاريف، والمادة 9(1) للبائع الذي تخضع جميع توريداته لنسبة الصفر.
- بعد التسجيل، هل يجب على الشخص إلغاء تسجيله أو يجوز له؟ الفقرات (1) و(3) و(6) و(8) من المادة الثالثة عشرة، مع الشرط الذي أضافه القرار 24-06-01 عام 2024.
ويمتنع المحرك عن الإجابة حين يجب أن يمتنع: شهر بلا قيود، أو تقدير غائب، يعطي unknown، ولا يعطي أبدًا "دون الحد".
المتطلبات المسبقة
- Node.js الإصدار 20 أو أحدث
- معرفة عملية بلغة TypeScript
- معرفة بطريقة تخزين نظامك لقيود المبيعات والمصاريف حسب الشهر
- لا حاجة إلى أي واجهة برمجية من هيئة الزكاة والضريبة والجمارك. لا شيء هنا يتصل بالهيئة؛ هذا هو الحساب الذي يقرر هل يجب عليك التسجيل
ملاحظة عن المصادر. أرقام المواد والاقتباسات مأخوذة من النص العربي الموحّد الحالي للائحة التنفيذية المنشور على موقع الهيئة، وهو يتضمن قرار مجلس الإدارة 24-06-01 بتاريخ 19 نوفمبر 2024. ملف الهيئة الإنجليزي طبعة أقدم ولا يتضمن هذا القرار. وحيث يختلف النصان فالنص العربي هو المعتمد. كل موضع يأخذ فيه المحرك بقراءة لا يحسمها النص مذكور في قسم "القراءات التي يأخذ بها هذا المحرك" أدناه.
الخطوة 1: إعداد المشروع
mkdir vat-threshold && cd vat-threshold
npm init -y
npm install --save-dev typescript tsx @types/node
mkdir src teststsconfig.json
{
"compilerOptions": {
"strict": true,
"target": "ES2022",
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true,
"noEmit": true,
"types": ["node"]
},
"include": ["src", "tests", "cli.ts"]
}كل مبلغ عدد صحيح من الهللات. اختبار حدّ يقارن ريالات بأعداد عشرية سيقرر يومًا ما أن 374,999.99 زائد 0.01 أقل من 375,000. الأعداد الصحيحة تجعل "بالضبط عند الحد" قيمة يمكن اختبارها.
الخطوة 2: الأشهر وقاعدتا التاريخ
تحسب اللائحة بالأشهر الميلادية وتستعمل قاعدتين للتاريخ، كلتاهما في المادة الثالثة:
- المهلة. «خلال ثلاثين (30) يومًا من نهاية ذلك الشهر». نهاية نوفمبر هي 30 نوفمبر، فآخر يوم هو 30 ديسمبر. ونهاية فبراير 2027 هي 28 فبراير، فآخر يوم هو 30 مارس.
- تاريخ النفاذ في المادة الثالثة. «اعتبارًا من بداية الشهر التالي للشهر الذي تم فيه تقديم طلب التسجيل». يتوقف على تاريخ تقديم الطلب فعلًا، لا على تاريخ تجاوز الحد.
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);
}الأشهر نصوص مثل "2026-11" لأن الفترة المحاسبية المقفلة تُعرَّف عادة هكذا، ولأن مقارنة النصوص بصيغة YYYY-MM ترتّبها ترتيبًا صحيحًا. والتواريخ تُحسب بتوقيت UTC حتى لا تحرّك المنطقة الزمنية للخادم مهلةً بيوم.
الخطوة 3: السجل، شخص واحد، كل السجلات التجارية
المدخل هو السجل الموجود لديك أصلًا: قيود المبيعات والمصاريف حسب الشهر. ثلاث قواعد تحدد ما يدخل في الرقم الذي يقارَن بالحد.
كل السجلات التجارية معًا. التسجيل مرتبط بالشخص لا بالسجل التجاري. في القرار VD-224-2020، قررت الدائرة الأولى للفصل في مخالفات ومنازعات ضريبة القيمة المضافة في الدمام أن التسجيل مرتبط بجميع السجلات التجارية للشخص. مالك مؤسستين فرديتين، كل واحدة دون الحد، قد يكون فوقه. لذلك يحمل السجل حقل cr للتدقيق، والمجاميع تتجاهله.
الأصول الرأسمالية مستبعدة. تستثني المادة 6(5) قيمة توريد الأصول الرأسمالية من مجموع التوريدات السنوية، «شريطة استخدام الأصل الرأسمالي في تشغيل وتعزيز استمرارية النشاط الاقتصادي» وفق سجل تجاري أو ترخيص، «وألا يكون قد تم الاحتفاظ بالأصل لكسب إيرادات إيجارية ... أو لتوريده لاحقًا». بيع سيارة التوصيل لا يدفعك فوق الحد. أما بيع شقة كنت تؤجرها فلا يشمله هذا الاستثناء، فسجّله بنوعه الحقيقي.
التوريدات المعفاة سياسة، لا ثابت. تقول المادتان الثالثة والرابعة إن القيمة تُحسب «وفقًا للمتطلبات المنصوص عليها في الاتفاقية». أغلب الأدلة المهنية تستبعد التوريدات المعفاة من الحساب، لكن اللائحة لا تقول ذلك في هاتين المادتين. لذلك يأخذ المحرك السياسة معاملًا، ويشغّل الاحتمالين في الخطوة 5.
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;
}النوع kind الذي تعطيه لكل قيد هو التصنيف الذي يجريه نظامك أصلًا لأغراض الفوترة. إن كان دليل الأصناف لديك لا يميّز بيع أصل رأسمالي من بيع مخزون، فأصلح ذلك أولًا: المراقب لا يكون أدق من التصنيف.
الخطوة 4: الاختبار في نهاية الشهر
هذا هو اللب. في نهاية الشهر asOf يجمع المحرك الاثني عشر شهرًا المنتهية بـ asOf (المادة الثالثة) والاثني عشر شهرًا المقدّرة ابتداءً من الشهر التالي (المادة الرابعة)، ثم يصنّف.
مقارنتان متشابهتان في الشكل ومختلفتان في الحكم:
- الإلزامي: "تجاوزت". تستعمل المادة الثالثة «تجاوزت». مبلغ 375,000.00 ريال بالضبط لا يتجاوز 375,000؛ ومبلغ 375,000.01 يتجاوزه. الشيفرة تستعمل مقارنة صارمة.
- الاختياري: "لا يقل عن". تستعمل المادة السابعة «لا يقل عن». مبلغ 187,500 بالضبط يكفي. الشيفرة تستعمل مقارنة شاملة، والمادة السابعة تحسب التوريدات أو المصاريف، وهذا مهم لمنشأة تنفق على التجهيز قبل أن تبيع شيئًا.
وتواريخ النفاذ تختلف أيضًا باختلاف المادة:
- المادة 3(2): بداية الشهر التالي للشهر الذي قُدّم فيه الطلب. يعرض المحرك التاريخ الذي ينطبق إذا قدّم الشخص طلبه في آخر يوم، أي أبعد موعد ممكن.
- المادة 4(2): «اعتبارًا من بداية الشهر الأول الذي يتوقع فيه أن تتجاوز قيمة توريداته السنوية في المملكة حد التسجيل الإلزامي». يعرض المحرك بداية أول شهر في فترة التقدير، وهي أبكر قراءة. الضريبة مستحقة من ذلك التاريخ سواء أُضيفت إلى الفواتير أم لا.
المادة 9(1) استثناء لشخص فوق الحد «إذا كانت جميع توريداته خاضعة فقط لنسبة الصفر بالمائة»، مثل مصدّر يبيع كامل إنتاجه في الخارج. لا يلزمه التسجيل، ويجوز له التسجيل اختياريًا. ريال واحد بالنسبة الأساسية داخل الفترة يكفي لفقدان الاستثناء، لذلك يكون الاختبار مساواة تامة بين مجموع نسبة الصفر والمجموع المحسوب.
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" };
}لاحظ ما يحدث عند نقص البيانات. أي شهر بين أول شهر نشاط وasOf بلا أي قيد يُبلَّغ عنه بـ MISSING_MONTH، لأن الشهر الخالي من القيود هو في الغالب شهر لم يُقفله أحد، لا شهر بلا مبيعات. الشهر الهادئ فعلًا يجب أن يحمل قيدًا صريحًا بصفر. والتقدير الغائب أو غير المتطابق يُبلَّغ عنه أيضًا. في الحالتين يظل المحرك يبلّغ عن تجاوز الفترة السابقة لأنه حقيقة قائمة، لكنه لا يقول below ولا voluntary_eligible، لأن البيانات الناقصة قد تخفي تجاوزًا.
الخطوة 5: احسب الرقم بالطريقتين
بما أن مسألة المعفى غير محسومة في المادتين اللتين تضعان الاختبار، فالمخرج الأمين هو الجوابان معًا وعلامة عند اختلافهما. المؤجّر الذي يدير متجرًا أيضًا هو الحالة المعتادة: الإيجار السكني معفى، وإضافته تغيّر المجموع.
أضف هذا إلى 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 };
}حين تكون dependsOnExempt صحيحة، لا ينبغي للنظام أن يقرر وحده. يجب أن يحيل الشهر إلى شخص، مع الرقمين، للحصول على رأي مكتوب. مقال المدونة يقول الشيء نفسه لأصحاب المنشآت؛ وهذه نسخة الشيفرة من تلك الجملة.
الخطوة 6: إلغاء التسجيل وفق المادة الثالثة عشرة
بعد التسجيل، تجيب الأرقام الشهرية نفسها عن سؤال آخر. تفصل المادة الثالثة عشرة بين ما يجب وما يجوز:
- 13(1): توقف النشاط. يجب على الشخص إلغاء تسجيله وتقديم الطلب خلال ثلاثين يومًا (13(4)). وأضاف القرار 24-06-01 أنه «في كافة الأحوال يجب على الشخص الخاضع للضريبة استيفاء جميع التزاماته الضريبية لتتم الموافقة على طلب إلغاء التسجيل»، وأدخل التنازل عن النشاط الذي يؤدي إلى التوقف الكامل.
- 13(3): واجب في نهاية أي شهر عند اجتماع الحالات الثلاث. التوريدات أو المصاريف خلال الاثني عشر شهرًا السابقة لا تتجاوز 187,500 ريال؛ وخلال الأربعة والعشرين شهرًا السابقة لا تتجاوز 375,000 ريال؛ ومن غير المتوقع أن تتجاوز 187,500 ريال في الأشهر اللاحقة.
- 13(6): جائز إذا لم تتجاوز التوريدات خلال الاثني عشر شهرًا السابقة 375,000 ريال ولم يُتوقع أن تتجاوزها في الاثني عشر شهرًا القادمة.
- 13(8): مقفل. الشخص الذي سجّل اختياريًا وفق المادة السابعة ولم يتوقف عن نشاطه لا يجوز له طلب إلغاء التسجيل قبل مضي اثني عشر شهرًا. التوقف عن النشاط هو الاستثناء.
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 };
}الالتزامات غير المستوفاة تُعاد بوصفها عائقًا، لا حالة. قد يظل الشخص ملزمًا بإلغاء التسجيل؛ لكن الطلب لن يُعتمد حتى يزول العائق. الفصل بينهما يتيح للنظام أن يعرض المهمة الصحيحة: "قدّم الإقرار الناقص ثم اطلب الإلغاء"، لا "لا شيء مطلوب".
الخطوة 7: اختبارات تثبّت القواعد
لكل قاعدة أعلاه اختبار، وكل اختبار يستعمل أرقامًا يمكنك التحقق منها يدويًا. اختبارات التسجيل تستعمل المتجر الوارد في مقال المدونة، الذي افتُتح في يناير.
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"]);
});شغّلها:
npx tsx --test tests/*.test.tsيجب أن تنجح الاختبارات العشرون كلها. وقبل أن تثق بمجموعة كهذه، اكسر الشيفرة عمدًا وتأكد أن اختبارًا يفشل. فعلنا ذلك في تسعة تغييرات: جعل المقارنة الإلزامية شاملة، وجعل المقارنة الاختيارية صارمة، و31 يومًا بدل 30، واحتساب بيع الأصول الرأسمالية، وتأخير تاريخ المادة الرابعة شهرًا، وتجاهل المادة 9(1) للمبيعات بالنسبة الأساسية، وتقصير فترة الأربعة والعشرين شهرًا إلى اثني عشر، وإزالة قفل المادة 13(8)، وتجاهل الأشهر الناقصة. كل تغيير منها أفشل اختبارًا واحدًا على الأقل.
الخطوة 8: تقرير نهاية الشهر
أداة سطر أوامر صغيرة تقرأ ملف السجل وتطبع سطرًا لكل نهاية شهر. السكربت أدناه يبني سجل المتجر، مع تقدير المالك في كل نهاية شهر بمبلغ 25,000 ريال شهريًا.
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.json2026-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يتجاوز مجموع الفترة السابقة الحدَّ في نهاية نوفمبر، وآخر يوم لتقديم الطلب هو 30 ديسمبر، والتقديم في ذلك اليوم يعطي تسجيلًا نافذًا من 1 يناير 2027. هذا هو جدول مقال المدونة، صادرًا الآن من الشيفرة.
غيّر الآن مدخلًا واحدًا: افترض أن تقدير المالك الصادق في نهاية يوليو كان "مثل يوليو تقريبًا، كل شهر"، أي 40,000 ريال شهريًا. عدّل تقدير يوليو إلى 4000000 هللة لكل شهر وشغّل مرة أخرى. يصبح سطر يوليو:
2026-07 190,000.00 480,000.00 mandatory apply by 2026-08-30 VAT due from 2026-08-01تجاوز الاختبار المستقبلي الحدَّ قبل الاختبار السابق بأربعة أشهر، ووفق أبكر قراءة للمادة 4(2) تكون الضريبة مستحقة من 1 أغسطس. في المنشأة النامية هذه هي الحالة المعتادة لا الاستثنائية: التقدير يتجاوز أولًا في الغالب. وقد قال حساب الدعم الرسمي للهيئة علنًا إن حد التسجيل الإلزامي للمنشآت الجديدة يُحسب على المبيعات المتوقعة للاثني عشر شهرًا القادمة. المراقب الذي يجمع الماضي فقط يفحص الاختبار الذي يصل ثانيًا.
الخطوة 9: ربطه بنظام حقيقي
المحرك صغير عن قصد. والعمل الذي يجعله مفيدًا يقع حوله.
شغّله عند الإقفال الشهري، لا بجدول خاص به. رقم الفترة السابقة لا معنى له إلا بعد إقفال الشهر. أطلق التقييم من حدث الإقفال، لكل عميل أو كيان غير مسجل، واحفظ النتيجة.
احفظ التقدير مع النتيجة. كل تقييم يُكتب مرة واحدة، مع التقدير الذي أنتجه، ولا يُعدَّل بعدها. إذا سجّلت الهيئة الشخص لاحقًا من تلقاء نفسها وفق المادة 6(1)، فتاريخ النفاذ هو ما تحدده المادتان الثالثة والرابعة. وسجلك لما كان متوقعًا، ومتى، هو الدليل على ما كان الشخص يعرفه في نهاية كل شهر.
غذِّ التقدير بأشياء موجودة. العقود الموقعة والاشتراكات المتكررة وأوامر الشراء المؤكدة مدخلات أفضل من نسبة نمو. حين يُوقَّع عقد في منتصف الشهر، يجب أن يتضمنه تقدير نهاية ذلك الشهر. هذه بالضبط الحالة التي كُتبت لها المادة الرابعة.
نبّه قبل المهلة، لا يوم انتهائها. افتح مهمة في اليوم الذي تصبح فيه الحالة mandatory، وتذكيرًا قبل applyBy بعشرة أيام، وتصعيدًا قبلها بيومين. وأحِل حالة unknown إلى من يُقفل الدفاتر، مع قائمة الأشهر الناقصة.
اجعل قرار التسعير جزءًا من المهمة. التسجيل يغيّر الأسعار. العقود المسعّرة "شاملة" ستُقتطع منها الضريبة، فيصبح 15 من كل 115 ريالًا مقبوضًا ضريبة. المهمة التي تقول "قدّم الطلب قبل 30 ديسمبر" يجب أن تسأل أيضًا "تضيف 15% أم تتحملها؟". حاسبة الضريبة تعرض الاتجاهين لأي سعر.
بعد التسجيل، سلِّم المهمة. الشخص المسجل يحتاج محركًا آخر: الإقرار الدوري. بنيناه في درس محرك إقرار ضريبة القيمة المضافة السعودي. ومن تاريخ النفاذ سيبدأ عملاؤك أيضًا بالتحقق من رقمك قبل خصم الضريبة التي تفرضها عليهم؛ أداة التحقق من الرقم الضريبي تعرض ما يرونه، ودرس بوابة الرقم الضريبي للمورد يبني هذا الفحص من جهتهم.
القراءات التي يأخذ بها هذا المحرك
حيث لا يحسم النص مسألة، يأخذ المحرك بقراءة ويصرّح بها. غيّرها إن كان مستشارك يقرؤها بغير ذلك.
- تاريخ النفاذ في المادة 4(2). «بداية الشهر الأول الذي يتوقع فيه أن تتجاوز قيمة توريداته السنوية» الحد. نقرؤه بداية أول شهر في فترة التقدير، وهو أبكر تاريخ ويوافق ردود الهيئة العلنية بشأن المنشآت الجديدة. وهناك قراءة لاحقة ممكنة: أول شهر يتجاوز فيه مجموع متحرك يشمل التقدير الحدَّ. التاريخ الأبكر هو الذي يُخطَّط على أساسه.
- التوريدات المعفاة. غير محسومة في المادتين الثالثة والرابعة. يحسب المحرك الاحتمالين ويضع علامة على الأشهر التي يتوقف فيها الجواب عليها.
- المادة 13(3)، "التوريدات أو المصاريف لا تتجاوز". نشترط أن يكون كلاهما ضمن الحد. إذا تجاوز أحدهما ظل الشخص مؤهلًا للتسجيل الاختياري، فلا ينشأ واجب الخروج.
- فترة المادة 13(3)(ج). النص العربي يقول «في أي شهر من الأشهر الإحدى عشر اللاحقة»، والإنجليزي "ذلك الشهر والأشهر الأحد عشر التالية". يستعمل المحرك تقدير الاثني عشر شهرًا نفسه المستعمل في المادة الرابعة، وهو يغطي القراءتين.
- شرط استيفاء الالتزامات في القرار 24-06-01. أدرجه القرار في الفقرة الأولى، حالة توقف النشاط، بعبارة «في كافة الأحوال». يضع المحرك علامة على الالتزامات غير المستوفاة في كل حالات الإلغاء احتياطًا، لأن كل إلغاء يحتاج موافقة الهيئة.
- غير مُنمذج. تتيح المادتان 6(2) و6(3) للهيئة، بناءً على طلب، تأجيل تاريخ النفاذ (على ألا يتجاوز بداية الشهر الذي يلي تاريخ تقديم الطلب) أو تقديمه. وتتيح المادة 9(2) للهيئة ضم الأشخاص المرتبطين بإشعار؛ عند استلامه، ضع قيود الشخصين في سجل واحد. وغير المقيمين (المادة الخامسة) يتبعون قاعدة مختلفة كليًا.
استكشاف الأخطاء وإصلاحها
الحالة unknown مع أن كل شهر فيه مبيعات. افحص مفاتيح الأشهر. القيد المفتاح بـ "2026-8" بدل "2026-08" لا يطابق أي شهر في الفترة، فيظهر شهره ناقصًا. وحّد صيغة المفاتيح عند الاستيراد.
مجموع الفترة السابقة يختلف عن ميزان المراجعة. ميزان المراجعة يشمل التخلص من الأصول الرأسمالية، والإيرادات خارج النطاق، وبحسب سياستك الإيرادات المعفاة. قارن بـ counted، لا بالإيرادات.
عميل له سجلان تجاريان يظهر بنتيجتين منفصلتين. أنت تشغّل المحرك لكل سجل تجاري. شغّله لكل شخص، مع قيود كل سجلاته في سجل واحد. التسجيل مرتبط بالشخص.
التقدير مرفوض بـ FORECAST_NOT_NEXT_12_MONTHS. يجب أن يبدأ التقدير في الشهر التالي لـ asOf وأن يغطي اثني عشر شهرًا بالضبط. التقدير المبني على السنة الميلادية لن يتطابق بعد يناير.
المحرك يقول mandatory لمصدّر. قيد واحد على الأقل في الفترة غير مصنّف zero. المادة 9(1) تشترط أن تكون جميع التوريدات بنسبة الصفر. بيع محلي واحد، ولو صغير، يُسقط الاستثناء.
مسجّل اختياري يظهر locked بعد سنة سيئة. تمنع المادة 13(8) الطلب اثني عشر شهرًا من تاريخ النفاذ ما دام الشخص يمارس نشاطه. ويجوز للهيئة مع ذلك إلغاء تسجيله وفق الفقرة الخامسة؛ أما هو فلا يجوز له الطلب.
الخطوات التالية
- اقرأ نسخة أصحاب المنشآت من هذه القواعد: التسجيل في ضريبة القيمة المضافة: اختبار 375,000 ريال
- ابنِ ما يأتي بعد التسجيل: محرك إقرار ضريبة القيمة المضافة السعودي
- لماذا الإعفاء صفة للتوريد لا للمنشأة: التوريدات المعفاة لا المنشآت المعفاة
- مواعيد الإقرار وقواعد التصحيح بعد التسجيل: مواعيد إقرار ضريبة القيمة المضافة وغراماته وتصحيحه
الخلاصة
حد التسجيل ليس حدثًا سنويًا. إنه اختبار في نهاية كل شهر، يجري مرتين، مرة على ما حدث ومرة على ما هو متوقع، على أرقام تشمل كل سجل تجاري يملكه الشخص وتستبعد بيع الأصول الرأسمالية. المحرك في هذا الدرس يفعل ذلك في بضع مئات من الأسطر، ويمتنع عن التخمين حين تنقص البيانات، ويعطي المهلة وتاريخ النفاذ المنطبقين، ويغطي طريق الخروج وفق المادة الثالثة عشرة بعد تعديل 2024. الجزء الصعب ليس الشيفرة. إنه تصنيف كل قيد وتقدير صادق، وكلاهما مكانه الإقفال الشهري.
تبني برامج محاسبة أو أنظمة تخطيط موارد للمنشآت السعودية، أو تمسك دفاتر عدد منها؟ ندمج فحوصًا كهذا في الأنظمة القائمة: السجل، ومصدر التقدير، ومحفّز نهاية الشهر، والتنبيهات. أرسل لنا كيف يخزّن نظامك المبيعات والعقود اليوم، وسنردّ عليك بالموضع الذي يناسب المراقب وما يحتاجه. تواصل معنا.