Somebody transposes two digits while adding a customer. Nothing objects. The record saves, invoices are issued against it for two months, and the problem surfaces when a batch of them is rejected by a system that does check.
By then the fix is not a corrected field. It is corrected invoices, which in a fiscal context means storno documents and an explanation.
The whole thing is preventable at the moment of typing, because the number validates itself.
The check digit
A Romanian CUI is between two and ten digits, and the last one is a control digit computed from the rest.
The algorithm: take the number without its last digit, right-align it against the key 753217532, multiply each digit by the key digit above it, and sum. Multiply the sum by 10 and take the remainder modulo 11. If that remainder is 10, the control digit is 0; otherwise it is the remainder.
function isValidCui(input) {
const digits = String(input).replace(/^RO/i, "").replace(/\D/g, "");
if (digits.length < 2 || digits.length > 10) return false;
const key = "753217532";
const control = Number(digits.at(-1));
const body = digits.slice(0, -1);
// Right-align the body against the key: a 6 digit body uses the last 6 of it.
const offset = key.length - body.length;
let sum = 0;
for (let i = 0; i < body.length; i++) {
sum += Number(body[i]) * Number(key[offset + i]);
}
const remainder = (sum * 10) % 11;
return (remainder === 10 ? 0 : remainder) === control;
}
That is the whole thing. It catches every single-digit typo and almost every transposition, which between them are the overwhelming majority of manual entry errors.
What it does not tell you
Be clear about the limits, because people over-trust check digits once they have one.
It does not tell you the company exists. A number can be arithmetically valid and belong to nobody. The check digit rules out typos, not fiction.
It does not tell you the company is VAT registered. That is a separate fact, it changes over time, and it is the thing that decides whether the identifier is written with the RO prefix on a document. A company can have a perfectly valid CUI and not be a VAT payer, and using the prefixed form for them is wrong.
It does not tell you the company is still active. Companies are struck off.
So the check digit is the first gate, not the only one. The second gate is a lookup against the public register, which tells you the registered name, the VAT status and the address as the state holds them. Do that at the point of entry too, and prefill the name from it rather than letting somebody type it, which incidentally removes an entire class of diacritic and spelling variation from your customer table.
Where to put the validation
At the input, immediately. The value of this check is entirely in when it fires. Same field, same second, while the person still has the paperwork in front of them. A validation that runs on submit, after eleven other fields, is worth a fraction as much.
At the database boundary as well. Client-side validation is a courtesy to the user, not a guarantee. Data arrives by import, by API, and by somebody pasting a spreadsheet. The rule belongs in one function that every path calls.
Not in the database as a constraint. Tempting, and a mistake. You will eventually need to store a foreign company with a VAT number that is not a CUI at all, or a partner whose identifier you have not yet confirmed, and a hard constraint turns that into an emergency migration. Validate on the way in, allow the field to hold what reality contains.
Normalising while you are there
Store one canonical form. Strip spaces, strip a leading RO, keep digits. Then hold the VAT status as a separate boolean, and derive the prefixed form when a document needs it.
The alternative, which is what most systems do accidentally, is a column containing RO12345678, 12345678, RO 12345678 and ro12345678 for the same four companies, none of which match each other in a join.
Why this is worth an article at all
Because it is four lines, it prevents a category of expensive downstream correction, and almost no Romanian business system does it.
That combination is the useful thing to notice. The cheap checks that nobody implements are almost always the ones where the cost of not implementing them lands on somebody other than the person writing the code: the accountant, in six weeks, in a different system. Which is exactly the kind of cost that never makes it into a requirements document.
