Skip to content

Documents and templates ​

A template is a document that can be signed. Have as many as you need, for any purpose — a merchant agreement, a data processing addendum, a settlement amendment. The module does not know or care which.

Settings → Contract templates. Requires signing_template.view to read, signing_template.manage to write.

What is not checked ​

The wording is not ours

Nothing checks what a document says. No clause list, no disclosure list, no country requirement. Contract wording is written by people who own that work, and a system that refused their document over content it does not own would be taking on a job it cannot do.

Publication refuses three mechanical problems only:

RefusedWhy
A value the document uses but does not declareNothing could fill it, so it would render blank — and a blank reads as a finished sentence
A table value inside a paragraphIt renders as a table, so it cannot sit in the middle of a sentence
A claimed language with no textA signer would read a document missing a clause, and nothing would say so
No sections at allThere is no document

Each is checked in every declared language: a value missing from the Arabic body is as real as one missing from the English.

Two languages ​

A document is written in one or more languages, and one of them governs. Both are declared by the author — nothing is inferred from what happens to be filled in.

For a Saudi merchant agreement that means Arabic and English, with Arabic governing: it leads the document, and it is the text a court reads if the two ever disagree.

A half-translated document cannot publish

If a document claims Arabic and a section has no Arabic text, publication is refused and names the sections.

This is not a judgement about wording. It is the mechanical question of whether the text a signer will actually read exists at all — without it, a merchant reads the Arabic and one clause is simply missing from their side, with nothing anywhere saying so.

Each section holds a heading and body per language, and the rendered document contains both — every block stating its own lang and dir.

Side by side, as the source sets it ​

A bilingual section is set in two equal columns, governing language first, no rule between them. That is how the Saudi agreement's Word source sets its articles, and it is what a signatory comparing a clause against its translation expects to find.

First in source order is first in the reading direction, so in an Arabic-governed document the Arabic is the right-hand column — which is where the source puts it too (its articles table carries bidiVisual, and its two cells are 5113 and 5077 twips, equal within a rounding error).

The columns re-align at every section boundary, so a long clause cannot push the two apart for the rest of the document. That was the original objection to columns, and per-section alignment answers it.

Two exceptions, both mechanical:

A section holding a tableSet full width, languages stacked. A table cannot be halved — and a fee schedule is already bilingual in its own right. The source does the same: its articles are a two-column table, its pricing offer is not
A frame narrower than two columnsStacked. The threshold is the reading measure, not a device: the source's columns are about 3.5 inches, roughly 300px, which sets some forty characters of Georgia at 12pt

Arabic sets right-to-left, and no webfont is fetched to do it — a document whose script depends on somebody else's uptime is not an archive.

Setting the direction is not setting the alignment

text-align inherits. An Arabic-governed document sets it on the root, so an English block that declared only dir="ltr" still drew its text flush right, ragged left — and a fee table's English column did the same, because [dir="rtl"] td matched every cell of a table sitting inside the Arabic block.

Both directions are now stated explicitly, in blocks and in table cells, and a cell's own direction beats the table's. This was invisible while the languages were stacked; putting them side by side is what surfaced it.

A prepared document keeps its own layout

Changing the renderer does not change a document already prepared. Its bytes are frozen and its digest still matches — so an envelope presented before this change still renders stacked, forever. That is the archive working, not a gap.

Values in two languages ​

A value may be supplied per language:

ruby
"merchant_legal_name" => { "en" => "Northwind Trading Co", "ar" => "شركة نورث ويند التجارية" }

A merchant's Arabic name is not its English one, and a registry returns both. A plain value is used for every language, which is right for a number or an IBAN.

Supplied in one language only, it is used in both — falling back beats printing a gap, because the name is the same thing either way.

The signatory's name works the same way: pass signatory.name as a per-language pair and each side of the document carries the right script. The column on the envelope is the fallback.

Versions ​

Content lives on versions, and a published version can never be edited.

  • A draft may be saved in any state, including half-written
  • Publishing freezes it — signatures made afterwards point at that exact wording
  • Amending opens a new draft, starting from the current wording so you amend rather than retype
  • A published version cannot be discarded either, because a signature may point at it

Each version stores a SHA-256 over its own canonical content. An immutable row does not strictly need one, but "immutable because nothing writes to it" is a claim about code and this is a claim about the bytes — it survives a restore, a migration and a console.

Values ​

A template declares what it needs filled in. There is no fixed vocabulary anywhere in the module, which is what lets one system produce documents for unrelated purposes.

Declare a value with a name, a label, and a kind:

Kind
TextA phrase, used inline: Between us and .
TableRows, on a line of its own with a blank line either side

Then use in the body. A name used but not declared stops publication.

What this replaces

The previous templating called record.send(name) where name came from a database column — arbitrary method dispatch driven by editable data. Any private reader, or destroy, was one template edit away.

Interpolation still goes through an allowlist. The allowlist is just declared per template now.

Always available ​

Facts about the signature itself, which a template neither declares nor can get wrong:

{{signatory.name}} · {{signatory.identifier}} · {{document.title}} · {{document.reference}} · {{document.prepared_on}}

Writing a document ​

Sections render in order. Each has a key, an optional heading, and a body; blank lines start new paragraphs. Plain fields rather than a rich editor, because the renderer produces its own markup and a WYSIWYG would generate markup it refuses.

Line endings are normalised on save. A textarea submits CRLF, and identical wording typed on Windows and on macOS has to produce the same content digest.

The editor shows what is still wrong while you write, rather than only when you try to publish.

The rendered document ​

Deterministic and self-contained — see Signing for why both matter and what is checked.

A URL written into a clause is a citation, not a dependency, and is allowed. The Saudi merchant agreement is obliged by its own Article Two to cite the Terms and the Privacy Policy by address. What is refused is a reference the document fetches at read time — a stylesheet, a font, an image, a script.

Seeing it ​

Every template has a Preview, reachable from the editor and from each version row. It renders the document through the same code that produces the bytes a signature is bound to — not an approximation. Only the values differ.

Values are shown as conspicuous stand-ins in «guillemets», so a screenshot of a preview cannot be mistaken for somebody's contract. The prepared-on date is the version's own rather than today's, so the same wording always previews the same.

A draft that cannot publish still previews — otherwise you could not see what you are writing. If it cannot render at all, the frame says why instead of erroring.

What the preview caught

Two things, both invisible without it.

English inside an Arabic-governed document was inheriting dir="rtl" from the document root, so the "First Party". rendered as ."the "First Party. Every language block now states its own direction.

And a declared language with no text was silently omitted — so the Saudi agreement, which declares Arabic as governing and at the time had none, rendered as a complete-looking English contract with <html lang="ar"> at the top. A declared language with no text now renders a visible gap, marked in that language. It cannot appear in anything signable, because publication refuses a declared language with no text; it only shows up in a draft, which is where it needs to be seen.

The document is framed rather than inlined, because it carries its own stylesheet. It is served under default-src 'none', so it can execute and fetch nothing.

The issuer's letterhead ​

A document carries the mark of whoever issued it: a logo and the issuer's name above the title, and the registered detail repeated at the foot — so a printed sheet separated from its first still says whose it is. The signatory's page carries the same mark, because a page asking somebody to sign has to say who is asking.

Supplied, never known

The signing module renders whatever letterhead it is handed and has no opinion about whose belongs on it — that is the same boundary that keeps merchant vocabulary out of it. Entities::MerchantAgreement is the only place in the codebase that names Moyasar to a contract.

A template with no issuer renders with no letterhead and is still a valid document.

It is archived on the envelope, not looked up at render time. An issuer's name or registered number can change, and a document has to keep the one it was presented under — a test asserts a later rename does not reach bytes already presented.

Inline SVG, not an image ​

The logo is inline markup with no src of any kind. A contract has to look the same in five years, and an image reference — even a data: URI — is a payload something in the delivery chain can strip. Inline markup cannot be fetched, cannot fail, and cannot be blocked. The self-containment assertion covers it.

The third inherited-direction bug in this document

SVG text inherits direction like any other text. In an Arabic-governed contract the wordmark anchored right and drew leftward out of its own viewBox, leaving a fragment that looked like a clipped image.

.letterhead__logo now states direction: ltr. A mark is artwork, not prose: it reads the same way whatever language surrounds it. The two before it were the English column drawing flush right, and the English fee labels doing the same inside an Arabic table.

Where the logo comes from ​

Today, one constant — correct while there is one licensed issuer, and wrong the moment there are two.

It is reached through Entities::MerchantAgreement.letterhead_for(account), and every call site already passes an account. When accounts carry their own logo and registered name, that becomes a change in one method rather than a hunt through the callers, with the constant remaining the fallback for an account that has set neither.

Moving a template between environments ​

A template is data. It lives in one account's database, and no screen carries it to another — so authoring the Saudi agreement in development and then needing it in staging, and later in production, is an export and an import rather than a deploy step.

bash
bin/rails signing:export TEMPLATE_ID=7 FILE=db/contract_templates/saudi_merchant_agreement.json
bin/rails signing:import FILE=db/contract_templates/saudi_merchant_agreement.json ACCOUNT_ID=2 \
  PUBLISHED_BY_USER_ID=3

bin/rails signing:templates lists what each account has, with each version's digest.

Commit the exported file. The wording then travels with the repository, the review that reads a code change reads a wording change, and every environment gets it by running one command rather than by somebody retyping a contract.

The digest is what makes this safe

Each version stores a SHA-256 over its own canonical content. The export carries it, and the import recomputes it from the file before writing anything. So an import either reproduces byte-identical wording or refuses — not "probably the same contract", but the same digest or nothing.

A clause dropped by a merge, or a quote mangled by an editor, is caught there rather than published as though it were the text that was reviewed. A test asserts the imported version renders byte-identically to the source.

Only the fields the digest covers are exported: titles, languages, sections and variables. Ids, timestamps and who published belong to the environment they happened in, not to the wording.

What it will not do ​

Stack duplicatesRe-importing the same wording is a no-op — but it still honours a publisher, so a second run can put an earlier draft in force
Publish on its ownWith no PUBLISHED_BY_USER_ID it stops at a publishable draft. Publication is attributed to a person, so it will not pick one for you
Accept any publisherThey must be in the target account and hold signing_template.manage
Force a half-translated document throughIt imports as a draft and names what is missing, exactly as authoring one does

The Saudi merchant agreement ​

Loaded as a draft by rake signing:saudi_merchant_agreement ACCOUNT_ID=…, transcribed from the Word source. Eight articles in both languages, the required-documents list, the pricing offer with its four fee tables, and the financial terms.

Three values vary per merchant — the merchant's name, its registration number, and the signatory's capacity — plus the four fee tables, because Article Six makes the fees a pricing offer rather than wording.

The Word source, not the PDF

The PDF's font subset carries a broken ToUnicode map: 183 occurrences of 42 distinct glyphs extract as Latin letters and digits sitting inside Arabic words, so الالكترونية came out as الالك45ونية. The .docx carries the same text as real codepoints — 5282 Arabic characters, zero presentation forms.

Article Eight makes the Arabic the governing version, so this is the difference between transcribing the text that prevails in a dispute and guessing at it. The draft published from the PDF had ten sections and no Arabic at all; the publication guard refused it and named all ten.

The fee tables are set once ​

Each fee table's rows carry the Arabic label, the amount and the English label — one bilingual table per fee group, exactly as the source sets it, rather than one table per language.

Stating an amount twice is how the Arabic half and the English half of a price list come to disagree, and the Arabic half is the one that governs. So the tables sit in the Arabic block and the English text says where to read them; the table it points at contains the English column.