Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 30 Oct 2026. What's built · Install 0.1.0
jadpo
Reference: Standard library

Standard library

Cards and identity documents

On this page

Cards and identity documents are numbers a person holds: a card's brand and last four digits, its expiry, a VAT number or tax id, a passport or a driving licence. Each number is checked against its country's published form, and the ones that identify a person are shown masked.

Here a shop keeps what it may of a customer's card once its payment provider has the number, checks a supplier's VAT number by its country's form, and records the passport a traveller shows:

example
type FormText = Text {}type NotAVatNumber = Failure {    kind: FailureKind.invalid_value}/// What the shop keeps of a card: its brand and last four.function card_on_file(card: PaymentCardNumber) -> MaskedCardNumber {    return money.mask(card)}/// A supplier's VAT number as its invoice prints it, with its country's/// code.function supplier_vat(raw: FormText) fails NotAVatNumber -> VatNumber {    var number: VatNumber = attempt text.parse(raw, as: VatNumber) {        malformed: NotAVatNumber        number_of_country: NotAVatNumber    }    return number}/// A card on file as a receipt shows it.function card_label(card: MaskedCardNumber, viewer: Locale) -> PresentationText {    return text.format(card, locale: viewer)}/// A document as a booking shows it: its number's last four.function document_label(held_document: IdentityDocument, viewer: Locale) -> PresentationText {    return text.format(held_document, locale: viewer)}
example
test "a card is kept as its brand and last four" {    var stored_card: MaskedCardNumber = card_on_file(PaymentCardNumber("4242424242424242"))    assert stored_card == MaskedCardNumber {        brand: CardBrand.visa        last_four: "4242"    }    assert card_label(stored_card, Locale.en_gb) == "Visa •••• 4242"    var charge_card: MaskedCardNumber = card_on_file(PaymentCardNumber("378282246310005"))    assert charge_card.brand == CardBrand.american_express    var runs_until: CardExpiry = CardExpiry {        year: 2028        month: 12    }    assert text.from(runs_until) == "2028-12"    assert text.format(runs_until, locale: Locale.en_gb) == "12/28"}test "a VAT number fits its country's form" {    var supplier: VatNumber = attempt supplier_vat("DE143454214")    assert supplier.country == Country.de    assert supplier_vat("DE143454215") fails NotAVatNumber    var greek_supplier: VatNumber = attempt supplier_vat("EL094259216")    assert greek_supplier.country == Country.gr    assert supplier_vat("GR094259216") fails NotAVatNumber}test "an identity document is shown by its number's last four" {    var passport: IdentityDocument = IdentityDocument {        kind: IdentityDocumentKind.passport        country: Country.fr        number: "12AB34567"        expires: "2031-05-31"    }    assert document_label(passport, Locale.fr_fr) == "•••• 4567"}

A number written out with its country is checked while compiling. A French passport's number is two digits, two letters and five digits, so this one is refused:

example
/// The passport a traveller showed at the desk.function shown_at_desk() -> PassportNumber {    return PassportNumber {        country: Country.fr        number: "123456789"    }}
text
error: This `PassportNumber` breaks its rule `number_of_country` --> main.jadpo:5:17  |5 |         number: "123456789"  |                 ^^^^^^^^^^^ not a number of the document's country  reason: A `PassportNumber`'s `number` is in the form its `country`'s passport office writes. A literal is checked while compiling.  next: Write a number in the country's form, or change `country` (agent)  rule: type.invalid_literal  help: https://jadpo.dev/docs/errors/type.invalid_literalcheck failed: 1 error, 1 file

Cards

  • CardBrand is the eight schemes that issue cards under ranges of their own worldwide: American Express, Diners Club, Discover, JCB, Maestro, Mastercard, UnionPay and Visa. No authority gives them codes, so each variant's code is its own name, american_express, as a Continent's is. It is shown by the scheme's own name in every locale, "American Express". It isn't personal data.
  • MaskedCardNumber is what a shop may keep of a card once its payment provider has the number: its last four digits, last_four, and its brand, a CardBrand that is none for a card no listed scheme issued. It has no single text form. Its JSON is its parts, and it is stored as a column per part. It is shown as a receipt prints it, Visa •••• 4242, or •••• 1234 with no brand. It is financial personal data, as the number is.
  • money.mask(card) makes one from a PaymentCardNumber. It is an operation rather than MaskedCardNumber(card), because a construction converts only between types with one representation (Creating values). It takes the brand from the longest issuer range the number starts with: Discover Global Network's published ranges for Discover, Diners Club International, JCB and UnionPay; Mastercard's for its 2-series; and the long-known ranges for Visa, American Express and Maestro.
  • CardExpiry is a year from 2000 to 2099 and a month, the year first so that it orders as a month does. Its canonical text is ISO 8601's 2028-12, and only that is read. It is shown as the card prints it, 12/28, in every locale; the bounds on the year keep that to one month. It is sent as its text and stored as two integer columns. It is financial personal data, as PCI DSS counts it. No operation says whether a card has expired on a given day.

Document numbers

A document's number is in upper case with no spaces or punctuation, as the payment numbers are, and it fits its country's published form. The named rule is number_of_country, or number_of_kind_and_country for an IdentityDocument:

  • where a literal writes the kind and the country, the rule is proved while compiling, as the passport example above shows;
  • where the value is built from other values, it is built with attempt;
  • in text and JSON, a number that doesn't fit breaks the rule by name.

A country that publishes no form takes any number within the type's bounds: 1 to 25 capitals and digits, the longest a US state's licence number may be by AAMVA's 2020 card standard, or 2 to 15 for a VAT number. So a number is never refused for its country alone. A release adds countries as their forms are published.

VAT numbers and tax ids

VatNumber is a Country and its number. Its canonical text is the one VIES, the EU's VAT checking service, uses: the country's code, then the number, GB980780684.

  • Greece's code is EL, so GR... is malformed.
  • Northern Ireland's XI isn't a country code, so a trader there has its GB number.
  • It is sent as that text, stored as a column per part, and shown as it is, because a business prints it on its invoices. It isn't personal data, as a US EIN isn't.

The forms come from VIES's table for the 27 member states; HMRC's for the United Kingdom, with its mod-97 check and the 9755 variant for newer numbers, a branch's three digits unchecked; Norway's and Switzerland's mod-11 organisation numbers with their MVA and MWST suffixes; and, outside Europe, Australia's ABN, whose check the ABR publishes.

The European Commission publishes no member state's check digit. So only Germany's (ISO 7064 MOD 11,10), France's (a key of two digits; one with a letter is unchecked) and Ireland's check letter are checked, since every number those authorities issue passes them. Ireland's old numbers with + or * aren't read.

TaxId is a Country and its number, with no scheme. The OECD's common reporting standard records a jurisdiction and a tax identification number (TIN), and code that needs one scheme has that scheme's type: UkUtr, UsSsn or UsEin. A number fits any of its country's forms, from the OECD's TIN page for each country and the EU's TIN algorithms (DG TAXUD's functional specification 5.03, 2017-02-24):

  • the United Kingdom's UTR, unchecked because HMRC publishes no check, or its National Insurance number;
  • Germany's tax identification number, with its repetition rule;
  • France's numéro fiscal;
  • Ireland's PPS number;
  • the US SSN, ITIN and EIN, by the prefixes the IRS assigns;
  • India's PAN;
  • Australia's tax file number, the older eight-digit form by its length only because its check isn't published;
  • Canada's SIN, by its Luhn check.

Passports, licences and identity documents

PassportNumber and DrivingLicenceNumber are a Country and a number.

  • Passports: the United Kingdom's nine digits (HMPO); Germany's nine characters of its 26 (the Federal Interior Ministry); the United States' nine digits, or a letter and eight (the State Department); and France's, Ireland's, India's, Canada's and Japan's forms as their offices state them.
  • Licences: Great Britain's by the pattern DVLA's own API checks; Northern Ireland's eight digits; Germany's eleven characters, its check digit unchecked because its algorithm is published only second-hand; and India's fifteen. France and Ireland publish no form, and the United States has none nationally, so each takes the bounds.

IdentityDocument is its kind, its country, its number, and the day it expires, which may be none, as Spain's DNI for someone over 70 never expires. The part is called kind because type is a reserved word. The kind is IdentityDocumentKind, a standard enum of passport, identity_card, driving_licence and residence_permit, each its own code, shown as its code.

The number fits its kind and country: a passport's or a licence's forms above, Germany's identity card, Ireland's Passport Card, and India's Aadhaar number by Verhoeff's check (UIDAI). No residence permit's form is published, and the United Kingdom, the United States and France publish no identity card's.

Masking, test values and shapes

Masked by text.format, to be safe. A TaxId, a PassportNumber, a DrivingLicenceNumber and an IdentityDocument are shown as their number's last four, •••• 6789, as a card number and an SSN are. Each is government_id personal data, which identifies its holder to the state. Code that must show the whole number reads number.

Test values are published ones where an issuer publishes them: DVLA's licence examples, the EU's TIN samples, the OECD's PAN, the ATO's own ABN and Stripe's test cards. Some come from a company printing its own: SAP's, TotalEnergies', Google Ireland's and Equinor's VAT numbers. Elsewhere they are made up to the rules. documents::DOCUMENT_EXAMPLES holds a valid and an invalid number for each rule, for GB, DE, FR, IE, US and one country outside Europe (Australia, India or Japan), for each type.

No shapes. type.standard_exists matches none of these objects, because { country, number } is too common in applications' own types to be a sign that a type is one.

The money group's records are under Money and payments, and BankAccount under Bank accounts.

Markdown for agents: /docs/cards-and-identity-documents.md.Built from Jadpo main at a707efb.