Portugal — AT (ATCUD + SAF-T PT)
Research doc for the Portugal fiscalization adapter. Internal reference
for engineers maintaining or extending rust/adapters/portugal/.
Status: Implemented (legacy Phase-0 wedge). Adapter at
rust/adapters/portugal/ (crate portugal).
Strategic context: Portugal was one of the four legacy launch
countries (FR/IT/PT/ES). The Portuguese regime is a hybrid: per-receipt
local computation (ATCUD validation code, signed hash, QR string) with
no real-time authority round-trip, plus a monthly SAF-T PT batch
file submitted to AT (Autoridade Tributária e Aduaneira). Software
itself must be certified by AT — the certification number prints on
every receipt. This combination lets us deploy in Cloud or Embedded
mode but adds a software-certification cost (~€5–10K, 4–6 months) on
top of merchant onboarding. See
apps/docs-internal/docs/architecture/portugal-adapter.md for the
narrative architecture and
apps/docs-internal/docs/ops/country-prioritization.md for the
business framing.
1. Regulatory scope
What must be reported
Two parallel obligations:
| Obligation | Mechanism | Frequency |
|---|---|---|
| Per-receipt fiscalization | Each invoice/receipt carries a locally-computed ATCUD code, a hash extract, and a delimited-string QR. Printed on the document. | Real-time at issuance |
| Monthly SAF-T PT | XML batch (SAF-T PT v1.04_01) covering all sales documents for the period, submitted via AT's e-fatura portal | Monthly, by day 5 of the following month |
The legal basis combines:
- Decreto-Lei 28/2019 — establishes the obligation to use AT-certified software for any taxpayer issuing invoices in Portugal.
- Portaria 195/2020 — defines ATCUD format, QR-code format (Annex), and series-validation flow.
- Portaria 302/2016 (amended) — defines the SAF-T PT v1.04_01 schema.
Who must report
Per Decreto-Lei 28/2019 Art. 2:
- Any taxable person (sujeito passivo) whose annual turnover exceeds €50,000 must use AT-certified billing software.
- Any taxable person, regardless of turnover, who issues invoices using software (i.e., not handwritten).
- This effectively captures all merchants with any meaningful invoicing volume.
Exemptions
- Below-€50,000 manual-paper-only operations remain technically permitted; these don't use software at all and are outside our reach.
- Specific carve-outs (regime simplificado for some agricultural flat-rate suppliers).
Mandatory dates
- 2008-01-01: Initial software-certification regime in force.
- 2017-07-01: Mandatory QR codes deferred — eventually came in with Portaria 195/2020.
- 2020-04-04: Portaria 195/2020 published — defines ATCUD + QR.
- 2022-01-01: ATCUD becomes mandatory on all printed invoices/receipts (after multiple deferrals from the original 2021-01-01 date).
- 2023-01-01: QR code becomes mandatory on all printed invoices/receipts.
The combined ATCUD+QR mandate is fully in force as of 2026-04.
Document types in scope
Mapped at rust/adapters/portugal/src/config.rs:101-114:
pub fn resolve_series_type(tx_type: &str, amount: i64, buyer_nif: &str) -> &'static str {
match tx_type {
"refund" => "NC",
"adjustment" => "ND",
"sale" => {
if is_b2b(buyer_nif) || amount > SIMPLIFIED_INVOICE_THRESHOLD {
"FT"
} else {
"FS"
}
}
_ => "FT",
}
}
SIMPLIFIED_INVOICE_THRESHOLD = 10000 minor units (€100,
config.rs:7).
| Code | Name | When |
|---|---|---|
FT | Fatura | B2B (buyer NIF present) or B2C amount > €100 |
FS | Fatura Simplificada | B2C and amount ≤ €100 |
FR | Fatura-Recibo | invoice + payment receipt in one (configurable per series) |
NC | Nota de Crédito | refund |
ND | Nota de Débito | adjustment / underbill |
The "final consumer" placeholder NIF is 999999990
(adapter.rs:8, qr.rs:8).
2. API surface
Per-receipt: no real-time API
There is no synchronous AT call per receipt. The adapter computes the ATCUD, hash, and QR locally; the receipt is then printed and the transaction is complete from a real-time perspective.
This is reflected in adapter.rs:255-264:
fn submission_window(&self) -> SubmissionWindow {
// Portugal: per-transaction ATCUD/QR locally. No real-time submission.
// Monthly SAF-T is a separate batch flow, not part of this interface.
SubmissionWindow::None
}
fn supported_deployment_modes(&self) -> &[DeploymentMode] {
&[DeploymentMode::Cloud, DeploymentMode::EmbeddedLocal]
}
Both deployment modes work because the per-receipt artefacts (ATCUD,
hash, QR) are computable from (NIF, doc_id, ATCUD, amount) —
nothing requires hardware-bound key material at the per-receipt
boundary.
Out-of-band: series registration with AT
Each document series (one per (taxpayer NIF, document type)) must
be registered once with AT through the e-fatura portal. AT returns a
validation_code (8+ alphanumeric chars; the atcud_code prefix)
unique to that series. The code is provisioned via the AT portal —
not via our adapter — and stored in
Config.series[doc_type].atcud_code (config.rs:33-37):
pub struct SeriesConfig {
pub prefix: String, // e.g. "FT 2026/"
pub atcud_code: String, // e.g. "CSDF7T5H"
}
Monthly SAF-T submission
The SAF-T PT XML is built by saft::build_saft_xml (saft.rs:67-78)
and submitted via AT's webservice / portal. The submission endpoint
itself is not wired into the adapter today — the adapter only
emits the XML. Submission is a downstream concern (cron job + a
separate AT-portal client).
The schema namespace (saft.rs:8-11):
urn:OECD:StandardAuditFile-Tax:PT_1.04_01. Version 1.04_01.
AT mock service for testing
The architecture doc references a MockATServer exposing:
POST /saft/submit— SAF-T submission.POST /atcud/register-series— series registration.POST /atcud/validate— ATCUD validation.
These are the legacy Go implementation's endpoints. The Rust
adapter's tests use file-based goldens
(testdata/conformance/portugal/at/golden/) rather than a running
mock server.
3. Authentication
Per-receipt: no authority auth
Nothing to authenticate against — the per-receipt path is local.
Software certification (one-time)
Every Portuguese fiscalization product must be AT-certified. The
certification number is a 4-digit string assigned by AT to a specific
release of the software. We pass it on every receipt
(adapter.rs:218):
cert_number: cfg.software_certificate_number.clone(),
It surfaces in the QR as field R (qr.rs:67-69) and on the printed
receipt header (mandatory under Portaria 195/2020 §6).
The cert number is per software release, not per merchant tenant. Renewal happens every release; AT requires re-certification when the fiscal pipeline changes materially.
SAF-T submission: mTLS
For the monthly SAF-T submission AT requires mutual TLS with a
certificate issued by AT (or via the Portal das Finanças PKI). This
is referenced in the location config as at_certificate_id
(config.rs:43-44):
#[serde(default)]
pub at_certificate_id: Option<String>,
Surfaced through CountryAdapter::required_certificate_id
(adapter.rs:361-368), which the orchestrator uses to pre-flight-check
that the cert is present before scheduling a submission.
Document signing chain (RSA-4096) — design vs current state
The Portuguese spec mandates an RSA-2048 minimum (typically
RSA-4096) document-signing chain: each invoice's hash incorporates the
previous invoice's hash, signed with the merchant's RSA key. The
chain produces the 4-character hash extract that prints on the receipt
and goes into the QR's Q field.
Current adapter implementation (adapter.rs:185-189):
let hash_input = format!("{};{};{};{}", loc.tax_id, doc_id, atcud, tx.amount);
let hash = Sha256::digest(hash_input.as_bytes());
let hash_hex = hex::encode(hash);
let hash_chars = &hash_hex[..4];
This is a simplified hash — SHA-256 over a 4-field string,
without RSA signing and without a chain reference. The architecture
doc (portugal-adapter.md) explicitly notes this is "planned for
production AT certification". For sandbox and goldens this passes; for
production the real RSA chain (signing the previous hash + this
document's content) needs to be added before AT will certify the
software for live use.
4. Payload shape
Per-receipt: 3 artefacts
For each transaction the adapter produces a PreparedRecord with no
submission payload but rich receipt-rendering data
(adapter.rs:265-345).
4.1 ATCUD
Format (atcud.rs:10-12):
pub fn format_atcud(validation_series: &str, sequential_number: i64) -> String {
format!("{}-{}", validation_series, sequential_number)
}
Example: CSDF7T5H-35.
The validation series (left side) is the AT-issued 8+ char alphanumeric
code per (NIF, doc_type) series. The right side is the gap-free
monotonic sequence within that series.
ATCUD prints on every receipt (mandatory header field per Portaria
195/2020 §6) and goes into the QR's H field.
4.2 Document hash extract (Q field)
Computed at adapter.rs:185-189 (see §3 — currently simplified). The
4 hex chars of the SHA-256 digest go into:
- The receipt body (typically next to "Hash:" label).
- The QR's
Qfield.
In production this should be the first 4 chars of SIGN(prev_hash + current_record_serialized, RSA private key), base64-encoded — that
is, the spec wants Q to anchor a chain. Today's adapter computes a
non-chained SHA-256 and uses the first 4 hex chars; this is enough for
sandbox conformance but not for production AT cert.
4.3 QR string (Portaria 195/2020 Annex)
Built by qr::generate_qr_content (qr.rs:12-72):
A:<seller NIF>*B:<buyer NIF>*C:<buyer country>*D:<doc type>*E:<status>*F:<doc date>*G:<doc id>*H:<ATCUD>*I1:PT*[I2-I8 tax breakdown]*N:<total tax>*O:<gross total>*[Q:<hash chars>]*[R:<cert number>]
Field reference:
| Field | Meaning | Notes |
|---|---|---|
A | seller NIF | mandatory |
B | buyer NIF | 999999990 for final consumer |
C | buyer country | ISO 3166-1 alpha-2; default PT |
D | document type | FT / FS / NC / ND |
E | document status | N (normal) / A (cancelled) |
F | issue date | YYYYMMDD |
G | document ID | series prefix + sequential |
H | ATCUD | <validation>-<seq> |
I1 | tax country region | PT for mainland, PT-AC Açores, PT-MA Madeira |
I2 | exempt taxable base | only if non-zero |
I3, I4 | reduced base, reduced tax | 6% mainland (or regional variants) |
I5, I6 | intermediate base, tax | 13% mainland |
I7, I8 | normal base, tax | 23% mainland |
N | total tax | required |
O | gross total | required |
Q | hash chars (first 4) | optional but printed in practice |
R | software certificate number | required |
Important: the QR is a delimited string, not a URL. The
PreparedRecord.render_hints flags this explicitly
(adapter.rs:320-331):
hint_items.push(RenderHintItem {
key: "qr".to_string(),
label: None,
value: qr.clone(),
placement: Placement::FooterRight,
mandatory: true,
kind: RenderKind::QrString, // not QrUrl
meta: serde_json::json!({ "min_size_mm": 30, "format": "AT-delimited" }),
});
RenderKind::QrString instructs the printer to encode the literal
delimited string as the QR payload — not as a URL or as JSON.
4.4 SAF-T PT v1.04_01 (monthly batch)
Built by build_saft_xml (saft.rs:67-78). Top-level structure:
AuditFile (xmlns="urn:OECD:StandardAuditFile-Tax:PT_1.04_01")
├── Header
│ ├── AuditFileVersion=1.04_01
│ ├── CompanyID, TaxRegistrationNumber, CompanyName
│ ├── FiscalYear, StartDate, EndDate
│ ├── CurrencyCode=EUR
│ ├── SoftwareCertificateNumber
│ └── ProductID
├── MasterFiles
│ ├── Customer[]
│ ├── Product[]
│ └── TaxTable
│ └── TaxTableEntry (TaxType=IVA, region, percentage, description)
└── SourceDocuments
└── SalesInvoices
├── NumberOfEntries, TotalDebit, TotalCredit
└── Invoice[]
├── InvoiceNo, ATCUD
├── InvoiceStatus (N | A | S | R)
├── Hash (full RSA sig in production), HashControl
├── InvoiceDate, InvoiceType
├── Line[]
└── DocumentTotals (TaxPayable, NetTotal, GrossTotal)
Document statuses (saft.rs:14-15):
| Status | Code | Meaning |
|---|---|---|
| Normal | N | active |
| Cancelled | A | anulado / void |
| Self-billing | S | auto-faturação |
| Summary | R | resumo |
VAT rates and codes (saft.rs:38-44):
| Code | Description | Mainland | Açores (PT-AC) | Madeira (PT-MA) |
|---|---|---|---|---|
RED | reduced | 6% | 4% | 5% |
INT | intermediate | 13% | 9% | 12% |
NOR | normal | 23% | 16% | 22% |
ISE | exempt | 0% | 0% | 0% |
Regional variants are emitted when tax_country_region is set to
PT-AC or PT-MA rather than the default PT.
5. Ordering and sequencing
Per-series gap-free counter
The adapter mints sequence numbers via the SequenceStore trait
(adapter.rs:40-76):
pub trait SequenceStore: Send + Sync {
fn next_sequence(&self, key: &str) -> Result<i64, String>;
}
The series_key is {NIF}:{doc_type} (config.rs:88-91):
pub fn series_key(nif: &str, series_type: &str) -> String {
format!("{}:{}", nif, series_type)
}
So a single taxpayer running with all 5 series gets 5 independent
counters. Each call to next_sequence is required to produce a
gap-free monotonic value — gaps trigger AT-302 ("ATCUD sequential
number is not sequential", errors.rs:48) at SAF-T time.
The bundled InMemorySequenceStore (adapter.rs:45-76) is for tests
only. Production deployments must wire a persistent store with strict
serialization:
- Cloud: Postgres with advisory locks (the legacy Go path).
- Embedded: SQLite with
BEGIN IMMEDIATE.
What about voids?
Voiding a document does not free its sequence. The next document
gets the next number; the voided one stays in the chain with status
A. This is captured at adapter.rs:191:
let doc_status = if tx.tx_type == "void" { "A" } else { "N" };
Within-month SAF-T ordering
SAF-T enforces a stricter property: for each (InvoiceType, Series)
the invoices must appear in SystemEntryDate order, and the
sequence numbers must be contiguous within the series for the period.
The XML builder honors caller-supplied order; the upstream caller is
responsible for sorting invoices by system_entry_date before passing
them to build_saft_xml.
Idempotency
Per-receipt: deterministic from inputs, so re-running prepare()
with the same (tx, loc, cfg) produces the same (ATCUD, hash, QR) —
but only if the sequence store has not advanced. In practice this
means we should not re-call prepare() on retry; instead, persist the
PreparedRecord and re-use it.
SAF-T submission: AT deduplicates on (taxpayer NIF, fiscal year, period, file hash). A retry of the exact same XML returns success on
the original submission. A modified resubmission (corrections after
the deadline) is a separate SAF-T file with status R (replacement).
6. Submission window and deployment mode
Submission window
SubmissionWindow::None for the per-receipt flow — there is no
submission. The monthly SAF-T flow runs out-of-band on a cron
schedule and is not modeled by this trait.
SAF-T deadline
By day 5 of the following month (Portaria 363/2010 as amended). Late submission is an administrative offense (€200–€10,000 per file, cited in the AT general infraction regime).
Deployment mode
Both Cloud and EmbeddedLocal are supported (adapter.rs:261-263).
There is no hardware-binding constraint on the per-receipt path. The
SAF-T mTLS cert can be brokered to Cloud-side KMS, so the monthly
flow is also Cloud-OK.
This is one of the cleaner countries from a deployment-mode perspective — neither the per-receipt path nor the monthly batch forces us off Cloud.
7. Sandbox access
AT operates a sandbox at "AT — Ambiente de Testes" alongside the production Portal das Finanças. Onboarding:
- Register a test taxpayer NIF on the AT test portal. Free; no real KYC.
- Register one document series per type (FT, FS, NC, ND) — AT
responds with a
validation_codeper series (theseries.<type>.atcud_codeconfig field). - Request a test mTLS cert from AT for SAF-T submission. The cert is delivered as PKCS#12 with a one-time PIN; capture immediately.
- Pick a software certificate number — AT issues this on software-cert renewal; for sandbox, AT provides "1234" as a placeholder cert number that the test endpoints accept.
See apps/docs-internal/docs/countries/portugal-sandbox.md for the
full step-by-step.
What we'll need
- A
.p12checked into our test-secrets vault for the SAF-T mTLS cert. - A test NIF + 5 ATCUD validation codes (one per series we exercise).
- A pinned
software_certificate_number(config field, mandatory). - An
.envtemplate with:PT_AT_NIF,PT_AT_SAFT_PFX_PATH,PT_AT_SAFT_PFX_PASSWORD,PT_AT_SOFTWARE_CERT_NUMBER, plus theseries.*.atcud_codeconfiguration entries.
8. Error model
What can fail at prepare()
| Variant | Source (adapter.rs:14-37) | Recovery |
|---|---|---|
PortugalError::Config(ConfigError) | bad config (missing legal name, invalid series type, missing prefix/atcud_code) | reject at registration via validate_config; merchant fixes config |
PortugalError::NoSequenceStore | adapter constructed without a SequenceStore | wiring bug — bind the store at startup |
PortugalError::Sequence(String) | sequence store backend error | typically transient (DB blip) — retry |
PortugalError::SeriesNotFound(String) | tx maps to a doc type for which no series is configured | merchant must register that series with AT and add it to config |
PortugalError::Serialization(serde_json::Error) | receipt JSON serialization | should not happen |
These map to AdapterError::Config / AdapterError::Internal
(adapter.rs:30-37).
Validation in validate_config
Per config.rs:49-85:
legal_namenon-empty.seriesnon-empty.- For every series entry:
doc_typein{FT, FS, FR, NC, ND}, non-emptyprefix, non-emptyatcud_codematching the^[A-Za-z0-9]{8,}$regex (atcud.rs:4). software_certificate_numberis required (struct field, non-optional).
NIF validation
validate_nif (nif.rs:6-23) implements the mod-11 check digit
algorithm. First digit must be one of 1, 2, 3, 5, 6, 7, 8, 9 (0
and 4 are reserved). The check digit is:
remainder = sum(digit[i] * (9-i) for i in 0..8) mod 11
check = 0 if remainder <= 1 else 11 - remainder
Used both for the issuer NIF (location tax_id) and any buyer NIF
passed in transaction metadata.
AT error catalogue
errors.rs ships a static map of AT codes (AT-001 through
AT-902) with English translations
(errors.rs:16-113).
Categories (errors.rs:126-142):
| 4th digit | Category |
|---|---|
0 | auth |
1 | nif |
2 | saft_structure |
3 | atcud |
4 | invoice |
5 | tax |
6 | signature |
7 | submission |
9 | server |
Selected codes:
| Code | Class | Description |
|---|---|---|
AT-001 | auth | invalid or expired authentication credentials |
AT-200 | saft_structure | SAF-T XML does not conform to schema |
AT-206 | saft_structure | TaxRegistrationNumber in Header does not match authenticated NIF |
AT-300 | atcud | ATCUD code is missing or malformed |
AT-301 | atcud | ATCUD validation series not registered with AT |
AT-302 | atcud | ATCUD sequential number is not sequential (gap detected) |
AT-303 | atcud | ATCUD sequential number already used |
AT-407 | invoice | duplicate invoice number in same series |
AT-601 | signature | digital signature verification failed |
AT-602 | signature | hash of previous document does not match chain |
AT-701 | submission | duplicate submission detected |
AT-703 | submission | rate limit exceeded, retry after cooldown period |
AT-900 | server | internal server error at AT, please retry |
Retriability (errors.rs:144-147):
pub fn is_retriable(code: &str) -> bool {
matches!(code, "AT-703" | "AT-704" | "AT-900" | "AT-901" | "AT-902")
}
So only rate-limit, "previous still processing", and server-class errors retry. ATCUD/auth/schema errors are operator-fix-required.
9. Implementation notes
Crate layout
rust/adapters/portugal/
├── Cargo.toml # crate "portugal"
└── src/
├── lib.rs # public re-exports
├── adapter.rs # Adapter, SequenceStore, InMemorySequenceStore, CountryAdapter impl
├── config.rs # Config, SeriesConfig, resolve_series_type, series_key
├── atcud.rs # format_atcud, validate_atcud_code, AtcudPair
├── qr.rs # generate_qr_content (Portaria 195/2020 Annex)
├── nif.rs # validate_nif (mod-11), generate_test_nif
├── saft.rs # build_saft_xml (SAF-T PT v1.04_01)
├── errors.rs # AT error code translations, error_category, is_retriable
└── types.rs # SaftInput DTOs, QrParams, QrLineItem
Trait fit
CountryAdapter::prepare():
- Resolve doc type from
(tx_type, amount, buyer_nif)viaresolve_series_type. - Look up series config (
cfg.series_for(doc_type)). next_sequence(series_key)for gap-free numbering.- Format ATCUD:
<validation_code>-<seq>. - Build doc ID:
<series.prefix><seq>(e.g.FT 2026/35). - Compute document hash (SHA-256 of
NIF;DocID;ATCUD;Amount, first 4 hex chars). - Map status:
Aiftx_type == "void", elseN. - Build QR content via
generate_qr_content(delimited string). - Emit
PreparedRecordwithqr_codeset,submission_payloadempty, render hints for ATCUD (header) + QR (footer-right).
CountryAdapter::submit() is a no-op — there is no per-receipt
submission.
CountryAdapter::required_certificate_id returns
config.at_certificate_id (for the SAF-T mTLS cert) when present.
Idiosyncrasies and gotchas
created_atis rendered asYYYYMMDDinPreparedRecord(adapter.rs:285). This matches AT's QRFfield expectation. Don't pass through the original RFC 3339 — AT rejects it.- Final-consumer NIF is hard-coded as
999999990(adapter.rs:8,qr.rs:8). This is the only NIF AT recognizes as "final consumer (no NIF given)" — don't substitute another value like999999999. - The "type" field on hash input is
tx.amount, not the gross total in EUR or the line-item sum (adapter.rs:186). This matches the legacy Go implementation's hash-extract behavior; verify when diffing against the Go reference. - Hash is currently SHA-256, not RSA-signed
(
adapter.rs:185-189). AT will reject this for production certification — see §3. The chain-signing migration is tracked separately. series_foris fallible — if the config lacks a series for the resolved doc type,prepare()errors out (adapter.rs:171). The validator atvalidate_configdoes not require all 5 doc types be configured — a merchant who never issues credit notes legitimately omitsNC. Make sure upstream knows: configuring onlyFTmeans refunds will fail atprepare().- The AT QR is a delimited string, not a URL
(
adapter.rs:328-331). Some merchants try to "make it a URL" by prefixinghttps://— that's wrong, AT rejects it on inspection. - Date format on the wire vs receipt: SAF-T expects
YYYY-MM-DDonInvoiceDatebutYYYYMMDDon the QR'sFfield. Both come from the sameTransaction.created_at— different formatters. - Regional variants (Açores
PT-AC, MadeiraPT-MA) are baked into the SAF-T tax-table emitter but not yet exposed at the QR level. If a merchant operates a Madeira location, thetax_country_regionmust propagate; today's adapter assumesPT(mainland). Tracked as a known limitation.
What's NOT in the adapter
- SAF-T submission HTTP client: not wired. The adapter only emits XML; sending it to AT is a downstream cron / portal flow.
- RSA chain signing: not implemented (see §3, §4.2). Required before production AT cert.
- AT mock server: legacy Go path (
internal/adapters/portugal/ mockat.go). Not ported; tests use file goldens instead. - NIF lookup against AT registry: the adapter validates format
only (
nif.rs:6-23). AT's authoritative NIF lookup is a separate webservice not used here.
10. References
Authority documentation
- Decreto-Lei 28/2019 — https://diariodarepublica.pt/dr/detalhe/decreto-lei/28-2019-119622094.
- Portaria 195/2020 (ATCUD + QR Annex) — https://diariodarepublica.pt/dr/detalhe/portaria/195-2020-134701066.
- Portaria 302/2016 (SAF-T PT v1.04_01) — defines the SAF-T schema; latest consolidated text at https://diariodarepublica.pt/dr/detalhe/portaria/302-2016-105536788.
- AT Portal das Finanças — https://www.portaldasfinancas.gov.pt/.
- AT Ambiente de Testes — sandbox portal, accessible from the Portal das Finanças "Ambiente de Testes" link after test-account registration.
- SAF-T PT XSD — distributed alongside Portaria 302/2016; mirrors exist on the AT website under Comunicações dos elementos das faturas.
Adapter source
rust/adapters/portugal/Cargo.toml— crate manifest.rust/adapters/portugal/src/adapter.rs—Adapter,SequenceStore,CountryAdapterimpl.rust/adapters/portugal/src/config.rs—Config,SeriesConfig,resolve_series_type,SIMPLIFIED_INVOICE_THRESHOLD,FINAL_CONSUMER_NIF.rust/adapters/portugal/src/atcud.rs—format_atcud,validate_atcud_code.rust/adapters/portugal/src/qr.rs—generate_qr_content(per Portaria 195/2020 Annex).rust/adapters/portugal/src/nif.rs—validate_nif(mod-11),generate_test_nif.rust/adapters/portugal/src/saft.rs—build_saft_xml,SAFT_NAMESPACE, status / type constants,tax_description.rust/adapters/portugal/src/errors.rs— AT error codes,error_category,is_retriable.
Internal docs
apps/docs-internal/docs/architecture/portugal-adapter.md— narrative architecture, mock server, certificate management.apps/docs-internal/docs/guides/portugal.md— integration guide (legacy; should migrate to external).apps/docs-internal/docs/countries/portugal-sandbox.md— sandbox onboarding steps.apps/docs/docs/guides/portugal.md— current customer-facing presentation.