You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: align document wire format with the live Vendus API; add test mode
Live validation against the real Vendus API surfaced wire-shape bugs that the
respx mocks hid (create_invoice had never actually worked end-to-end):
- Line items now send `tax_id` (a VAT category — new `TaxCategory` enum) instead
of the rejected `tax_rate`; `discount_percentage` instead of `discount`; `id`
instead of `product_id`. The real API returned P001 for the old field names.
- `cancel(document_id)` no longer sends `notes` and drops the `reason` argument —
the document PATCH endpoint accepts only status/mode (no reason field exists).
Added:
- `mode` parameter (`DocumentMode` enum) on every create_* method to issue
non-fiscal test-mode documents that Vendus does not report to the AT.
- `Document.tax_authority_id` — empty until the AT is notified.
- Live integration tests (excluded by default): create in test mode and
read-only list/get against real documents.
Docs:
- Replaced the unsourced "Sandbox" section with a sourced "Testing" section.
- README gains an honest per-operation unit/live validation matrix.
- CLAUDE.md: "Always Honest, Never Assume" rule, eupago-reference.md adopted as
the build playbook, R1 vocabulary updated, Sandbox TBD resolved.
BREAKING CHANGE: DocumentItem.tax_rate -> tax_category; cancel() no longer takes reason.
Copy file name to clipboardExpand all lines: CLAUDE.md
+47-2Lines changed: 47 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -8,6 +8,47 @@ Unofficial Python SDK for the [Vendus](https://www.vendus.pt) invoicing API (Por
8
8
9
9
Vendus is an AT-certified (Autoridade Tributária) invoicing/POS SaaS. The Vendus backend handles all communication with the AT (SAF-T, ATCUD, QR code, document hash). This SDK talks to Vendus; Vendus talks to AT. The SDK never communicates with the AT directly.
10
10
11
+
## Working Principle: Always Honest, Never Assume (non-negotiable)
12
+
13
+
This rule overrides convenience. It applies to code, comments, docs, commit messages, and every reply to the user.
14
+
15
+
-**State only what is verified.** If something is not confirmed, say so explicitly and label it as an assumption or unknown — never present a guess or general knowledge as established fact.
16
+
-**Cite the source for any claim about the Vendus API or AT behavior.** No source → mark it `TBD` in the code/docs and flag it for verification. Do not write unsourced assertions into user-facing docs.
17
+
-**When asked "how do we know X?"**, if there is no verified source, say so plainly instead of rationalizing after the fact.
18
+
-**When uncertain, verify or ask before acting.** Don't fill gaps with plausible-sounding details.
19
+
-**No contradictions between artifacts.** If the docs claim something the code/CLAUDE.md still lists as `TBD`, that is a bug — resolve it, don't paper over it.
20
+
21
+
> Origin: the docs once stated "Vendus has no public sandbox" as fact, with no cited source, while CLAUDE.md still listed sandbox as `TBD — investigate`. That unsourced assertion is exactly what this rule forbids.
22
+
23
+
## Build Playbook — `eupago-reference.md` is the foundation
24
+
25
+
The canonical playbook for building this SDK is [`eupago-reference.md`](eupago-reference.md) at the repo root — the distilled engineering discipline from the sibling `eupago-python` SDK. **Read it before any substantial work.** It is the *why*; the rules in this file are the Vendus-specific *how*.
26
+
27
+
-**R1–R15 below are the Vendus application of that playbook.** Adapt, don't copy: where the Vendus domain demands a different choice, diverge **deliberately and document it** — e.g. R3's conditional POST retries vs. the playbook's blanket no-retry, because Vendus accepts `external_reference` as a dedup anchor.
28
+
- The playbook's identity, architecture, naming, money/PII/validation and quality rules are already encoded as R1–R15 and the Architecture section. The two disciplines below are imported here explicitly because they are **not** yet encoded elsewhere in this file and this project has already been bitten by their absence.
| Unit |`pytest` + `respx`| every commit / CI | guard the **exact wire body**, validation, sync/async parity |
37
+
| Live |`pytest -m integration`| on demand | prove the SDK works against the **real Vendus API** end-to-end |
38
+
39
+
- Unit tests **assert the exact JSON sent on the wire**, not just the return value — that is how latent field-name/shape bugs are caught (`body = json.loads(route.calls[0].request.content); assert body == {...}`).
40
+
- Live tests live in `tests/integration/`, are marked `@pytest.mark.integration` (excluded from the default `pytest` run), and **auto-skip** when `VENDUS_API_KEY` is absent — no false failures on machines without creds.
41
+
-**One live test per operation**, exercising the full loop (SDK → Vendus → parse). Run them against the **test-mode register configured in `.env`** (`VENDUS_REGISTER_ID`, a register whose `mode` is `tests`) so live tests issue **non-fiscal** documents that are never reported to the AT. Concretely: assert a created test document comes back with an empty `tax_authority_id` (that field is only set once Vendus has communicated the document to the AT).
42
+
-**"If you didn't run it against the real Vendus API, it isn't done."** The Vendus `.doc` reference pages describe what *should* happen; verify the actual wire shape live before claiming an operation works. This is the operational form of the *Always Honest, Never Assume* rule above.
43
+
44
+
### Honesty in status reporting (`eupago-reference.md` §4, §8.1, §10.3)
45
+
46
+
- README / CHANGELOG / roadmap use a **per-operation matrix** (Unit ✅ / Live ✅), never a blanket "service done".
47
+
- Never mark a row **Done** without a live test — or a live test that **skips with a documented reason**. A skipped-with-reason test is honest; a green test that never hit the API is a trap.
48
+
- When the upstream docs turn out wrong or incomplete: fix the SDK, add a unit test asserting the **corrected** wire body, then the live test passes — and record the divergence (with the Vendus error it fixed) in the CHANGELOG.
49
+
50
+
For situations not covered here — webhooks, multiple identifiers for one resource, form-vs-JSON bodies, operations that don't return a field the docs promise — consult `eupago-reference.md` §7–§8 when they arise.
51
+
11
52
## Scope (v0.1.0 — MVP)
12
53
13
54
- Issue invoices (FT)
@@ -102,6 +143,7 @@ The Vendus API uses Portuguese-influenced naming (`fiscal_id`, `amount_gross`).
102
143
| Document number |`number`|`number`|`str`|
103
144
| Document type |`type`|`type`|`DocumentType` enum |
104
145
| Subtype |`subtype`|`subtype`|`str`|
146
+
| Working mode |`mode`|`mode`|`DocumentMode` enum |
105
147
| Date issued |`date`|`date`|`datetime`|
106
148
| Local time |`local_time`|`local_time`|`datetime`|
107
149
| System time |`system_time`|`system_time`|`datetime`|
@@ -110,13 +152,16 @@ The Vendus API uses Portuguese-influenced naming (`fiscal_id`, `amount_gross`).
110
152
| Tax amount |`tax_amount`| (derived) |`Decimal`|
111
153
| AT hash |`hash`|`hash`|`str`|
112
154
| ATCUD |`atcud`|`atcud`|`str`|
155
+
| AT document ID |`tax_authority_id`|`tax_authority_id`|`str`|
- Sandbox: **none.** Vendus has no separate sandbox host. Testing is done via a document-level test mode — pass `mode=tests` on a create call, or use a register configured in `tests` mode (new accounts default to this). Test documents are non-fiscal and not communicated to the AT (their `tax_authority_id` stays empty). Sources: [documents.doc](https://www.vendus.pt/ws/v1.1/documents.doc), [registers.doc](https://www.vendus.pt/ws/v1.1/registers.doc), [Modo de Formação/Testes](https://www.vendus.cv/ajuda/modo-formacao-testes/). Not yet live-verified: whether a per-request `mode=tests` overrides a `normal` register.
The wire format of every operation is asserted by unit tests (respx mocks). Live validation runs against the real Vendus API in **test mode** (`mode=tests`) — non-fiscal documents that are never reported to the AT:
|`create_invoice_receipt` (FR) | ✅ | ⚠️ not yet run live |
179
+
|`create_credit_note` (NC) | ✅ | ⚠️ not yet run live |
180
+
|`get` / `list`| ✅ | ✅ read-only, against real documents |
181
+
|`cancel`| ✅ | ⚠️ not live-validated (see note) |
182
+
183
+
> Test-mode documents ("Modo de Formação") are non-fiscal and never reported to the AT. Vendus stores them in a separate space, so they can't be retrieved or cancelled via `/documents/{id}`. Live `get`/`list` are validated read-only against real documents; `cancel` is not live-validated because voiding a real fiscal document is destructive.
184
+
171
185
## Why This SDK
172
186
173
187
-**Fully typed** — `mypy --strict` passes, `py.typed` marker included. Full autocomplete in VS Code and PyCharm.
1.**Full vs partial NC:** if refunding the whole amount, replicate all items. For partial refunds, include only the items/quantities to credit.
87
-
2.**NC is not cancellation:** an NC **credits** the value but keeps the original document. To fully void, use `client.documents.cancel(id, reason)` instead.
87
+
2.**NC is not cancellation:** an NC **credits** the value but keeps the original document. To fully void, use `client.documents.cancel(id)` instead.
88
88
3.**Consistent client:** if the original was to Final Consumer, the NC should also omit `client`.
1.**NC total vs parcial:** se devolves o total, replica todos os itens. Se devolves uma parte, inclui só os itens/quantidades a creditar.
87
-
2.**Não é cancelamento:** uma NC **credita** o valor mas mantém o documento original. Para cancelar completamente, usa `client.documents.cancel(id, reason)` em alternativa.
87
+
2.**Não é cancelamento:** uma NC **credita** o valor mas mantém o documento original. Para cancelar completamente, usa `client.documents.cancel(id)` em alternativa.
88
88
3.**Cliente coerente:** se o original foi a Consumidor Final, a NC também deve omitir `client`.
1.**Immediate payment:** FR assumes payment happens at issue time. If you bill on credit, use `create_invoice` (FT) and issue the receipt (RC) when the client pays — RC comes in a future version.
76
-
2.**Cancellation:** same API (`client.documents.cancel(id, reason)`).
76
+
2.**Cancellation:** same API (`client.documents.cancel(id)`).
77
77
3.**Credit note:** an FR can be credited via `create_credit_note` referencing the FR's `id`.
1.**Pagamento imediato:** a FR pressupõe que o pagamento ocorre no momento da emissão. Se faturas a crédito, usa `create_invoice` (FT) e emite o recibo (RC) quando o cliente pagar — RC chega numa versão futura.
76
-
2.**Cancelamento:** mesma API (`client.documents.cancel(id, reason)`).
76
+
2.**Cancelamento:** mesma API (`client.documents.cancel(id)`).
77
77
3.**Nota de crédito:** uma FR pode ser creditada via `create_credit_note` referenciando o `id` da FR.
0 commit comments