A downloaded invoice can have a different filename after an account language change. Consider a design scenario: a collector accepts only invoice-prefixed PDFs, so a valid document with a localized name becomes a missing item. A second download in English then looks like a separate expense. The archive has let a presentation detail define document identity.

I work on BrowserAct. Our Canva guide suggested an implementation worth isolating: preserve the supplier filename for traceability, but keep it outside the archive key. The example below plans an internal storage key and hashes supplied bytes. Local checks use fictional metadata and deliberately synthetic byte strings, not actual invoices or valid PDF fixtures. No Canva account, API, download or Bot is tested here.

Why filename rules are the wrong collection boundary

The Canva source guide, based on help checked November 9, notes that downloaded invoice filenames follow the user's language setting. It also separates the purchase email, active team and billing channel. A member's own purchase history and a team-wide invoice view are different scopes.

Website purchases use the documented Orders and invoices route. App Store or Google Play purchases need the respective store's document route. An English-looking PDF cannot resolve any of these distinctions. Nor does a localized name prove that the file belongs to the correct purchase.

The same guide says invoice copies can arrive by email. Browser downloads and email delivery may therefore yield two observations of the same document. Retaining both delivery observations is useful; treating each filename as another invoice is not.

Separate three values in the archive record

Keep a reviewed document identity, the original filename and a content digest. In this application design the identity contains a stable purchase-account reference, team reference and invoice ID, plus the issuer route. Those are internal contract fields, not a Canva API schema or a claim that the same hierarchy fits every seller.

The adapter must resolve and review those references before planning storage. A display name, design URL or company email domain is not a stable account identifier. Preserve the exact identifier spelling; the function does not trim or case-fold invoice IDs, even though it rejects empty or whitespace-only values.

The filename stays as metadata exactly as observed. It is never interpolated into a path. The output path uses fixed text and hexadecimal digests, so a localized name, slash or traversal-like string cannot choose a directory through this function. That is a naming property, not a complete secure-storage implementation.

import hashlib import json def plan_invoice_archive(record, payload): identity_fields = ("purchase_account_ref", "team_ref", "invoice_id") if any(not isinstance(record.get(k), str) or not record[k].strip() for k in identity_fields): return {"status": "review_identity"} if record.get("purchase_channel") != "canva_website": return {"status": "review_billing_channel"} if record.get("document_kind") != "invoice_pdf": return {"status": "review_document_kind"} if record.get("document_match_reviewed") is not True or not record.get("review_ref"): return {"status": "review_document_match"} if not isinstance(record.get("original_filename"), str): return {"status": "review_filename_metadata"} if type(payload) is not bytes or not payload: return {"status": "review_payload"} identity = {k: record[k] for k in identity_fields} identity["issuer_route"] = "canva_website" identity_bytes = json.dumps(identity, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8") identity_key = hashlib.sha256(identity_bytes).hexdigest() content_key = hashlib.sha256(payload).hexdigest() return { "status": "planned_only", "relative_key": f"canva/{identity_key}/{content_key}.pdf", "identity": identity, "original_filename": record["original_filename"], "sha256": content_key, "bytes": len(payload), "review_ref": record["review_ref"], } 

The review flag and reference are an upstream trust boundary. This function does not inspect PDF syntax, authenticate a supplier document, extract the invoice ID, check permissions or confirm that the bytes match the metadata. Production ingestion must perform those checks and retain the review evidence. Giving arbitrary bytes a .pdf suffix does not make them a PDF.

Likewise, the digest identifies byte content; it does not establish the financial meaning of those bytes. Two PDFs can represent one invoice while differing in rendering or metadata. Changed bytes produce another content version under the same identity directory, not an automatically new payable item.

Make invariants visible in the tests

Change in the input Expected plan Interpretation
Filename language changes, identity and bytes stay equal Same relative key Filename is presentation metadata
Only the original filename contains path separators Same relative key Raw filename never chooses storage location
Same invoice ID belongs to another team Different identity directory Team context remains part of identity
Same identity has different bytes Different content key in the same directory Preserve a candidate version for review
Purchase channel is a mobile store Review billing channel Do not file it under the website issuer route
Identity, match review or payload is missing Review the specific boundary No successful archive claim

The synthetic checks exercise these properties with English, Turkish, Chinese, accented and path-like names, plus wrong channels, missing fields, strict boolean handling and empty or non-bytes payloads. Hashing those synthetic bytes is a real local computation. It is not PDF parsing, supplier authentication or an account download benchmark.

A storage plan is still not a saved document

A storage worker must write the retained bytes, verify the resulting object and commit an artifact receipt. Use an atomic or conditional create operation, scoped access controls and a manifest transaction suitable for the storage system. This pure function neither writes files nor protects against filesystem symlinks, concurrent writers or an incorrect storage-root configuration.

If the key already exists, compare the retained object's digest before linking another delivery observation. Do not overwrite an existing object based only on the original filename. Also do not suppress a second observation merely because its bytes match: the delivery record can explain when the same invoice was recovered from email or the browser.

For BrowserAct or another collector, a Canva-specific adapter would still need authorized purchaser and team selection, correct invoice matching, real PDF delivery and independent archive verification. Keeping localized filenames outside the key gives that future test a precise invariant while preserving the source name and original bytes for the reviewer.