Wrap the eight invitation-key downloads
`GOALS.md` listed these as "eight routes that take no auth token, so they do
not fit the authenticated request path and need a different entry point."
That was wrong on every count, and wrong in the way goal 6 exists to catch:
the premise came from the route names rather than the route file.
All eight sit inside `routes/api.php`'s `api/v1` group, under its
`token_auth` middleware, and the controllers' own OpenAPI blocks declare
`X-API-TOKEN`. So they needed no new entry point — just a macro, alongside
`entity_action_methods!` and for the same four entities. What the item had
actually described is the client-portal family in `routes/client.php`, which
is genuinely token-less, is not under `/api/v1`, and stays unwrapped for
that reason; it is now the gap entry in `COVERAGE.md`.
`download_{invoice,quote,credit,purchase_order}_by_invitation_key` returns
the PDF and `download_e_*_by_invitation_key` the XML e-document. These take
a contact's invitation key rather than a record id, so the server renders in
that contact's locale — a client with contacts in two languages gets a
different document per key, which the id-based `download_*` cannot express.
Two things here are easy to get wrong, so both are pinned by tests. The
entity segment is singular (`invoice/{key}/download`), the only route family
in the API that departs from the plural CRUD path. And an unknown key is a
400 carrying `no record found`: the controllers return that directly instead
of raising `ModelNotFoundException`, so the status matches the rule for a
missing record but the message does not.
The smoke test's read tier now downloads an invoice by invitation key,
taking the key from the `invitations` array the list route already returns.
It is there to re-check the token claim against a real server rather than
against a second reading of the source. The `download_e_*` four are left
out: they render from e-invoice settings a test instance need not have.
Also in here, unrelated: a rustfmt fix to a `match` arm in
`examples/smoke_test.rs` that was already off-format on main, so
`cargo fmt --check` — and with it CI's `test` job — was failing before this
change. It could not be split out cleanly, being in a file this change also
touches.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -767,6 +767,52 @@ macro_rules! entity_action_methods {
|
||||
};
|
||||
}
|
||||
|
||||
/// Generates the two invitation-key downloads for one entity.
|
||||
///
|
||||
/// A different route family from [`entity_action_methods`], in two ways that
|
||||
/// are easy to trip over. The path segment is **singular** — `invoice/…`,
|
||||
/// not `invoices/…`, the only place in the API that does this. And the value
|
||||
/// in it is an *invitation key*, the per-contact token that appears in a
|
||||
/// client-portal link, not a record id; passing a record id gets the 400
|
||||
/// below, since no invitation has that key.
|
||||
///
|
||||
/// They are still authenticated. The routes sit inside `routes/api.php`'s
|
||||
/// `api/v1` group, under its `token_auth` middleware, so they go through the
|
||||
/// client's ordinary request path like any other call. The invitation
|
||||
/// downloads that genuinely take no token are the separate client-portal
|
||||
/// routes in `routes/client.php`, which are not wrapped — they are not under
|
||||
/// `/api/v1`, so they do not fit a `base_url` that ends there.
|
||||
macro_rules! invitation_download_methods {
|
||||
($download:ident, $download_e:ident, $path:literal, $e_action:literal) => {
|
||||
/// Downloads the rendered PDF for the record an invitation key
|
||||
/// points at.
|
||||
///
|
||||
/// This renders for one *contact* rather than one record, which is
|
||||
/// the reason to prefer it over the id-based `download_*` method:
|
||||
/// upstream sets the locale from the contact the invitation belongs
|
||||
/// to, so a client with two contacts in different languages gets a
|
||||
/// different document per key.
|
||||
///
|
||||
/// An unknown or already-deleted key is a `400` carrying
|
||||
/// `no record found`, not a `404`.
|
||||
pub async fn $download(&self, invitation_key: &str) -> Result<Vec<u8>> {
|
||||
self.get_raw(&format!("{}/{}/download", $path, seg(invitation_key)?))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Downloads the XML e-document for the record an invitation key
|
||||
/// points at.
|
||||
///
|
||||
/// The bytes are `application/xml`, not a PDF; which standard they
|
||||
/// follow (Factur-X, ZUGFeRD, Peppol, …) depends on the company's
|
||||
/// e-invoice settings. Errors are as above.
|
||||
pub async fn $download_e(&self, invitation_key: &str) -> Result<Vec<u8>> {
|
||||
self.get_raw(&format!("{}/{}/{}", $path, seg(invitation_key)?, $e_action))
|
||||
.await
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl InvoiceNinjaClient {
|
||||
resource_methods!(
|
||||
list_clients,
|
||||
@@ -1051,6 +1097,34 @@ impl InvoiceNinjaClient {
|
||||
"purchase_orders",
|
||||
PurchaseOrder
|
||||
);
|
||||
|
||||
invitation_download_methods!(
|
||||
download_invoice_by_invitation_key,
|
||||
download_e_invoice_by_invitation_key,
|
||||
"invoice",
|
||||
"download_e_invoice"
|
||||
);
|
||||
|
||||
invitation_download_methods!(
|
||||
download_quote_by_invitation_key,
|
||||
download_e_quote_by_invitation_key,
|
||||
"quote",
|
||||
"download_e_quote"
|
||||
);
|
||||
|
||||
invitation_download_methods!(
|
||||
download_credit_by_invitation_key,
|
||||
download_e_credit_by_invitation_key,
|
||||
"credit",
|
||||
"download_e_credit"
|
||||
);
|
||||
|
||||
invitation_download_methods!(
|
||||
download_purchase_order_by_invitation_key,
|
||||
download_e_purchase_order_by_invitation_key,
|
||||
"purchase_order",
|
||||
"download_e_purchase_order"
|
||||
);
|
||||
}
|
||||
|
||||
/// Documents, reports, and the remaining endpoints that do not follow the
|
||||
|
||||
Reference in New Issue
Block a user