Skip to content

Migrate 2024–2026 bookkeeping PDFs through an idempotent document API #117

Description

@alexeygrigorev

Migrate 2024–2026 bookkeeping PDFs through an idempotent document API

Status: pending
Tags: enhancement, migration, backend, data, testing, P1
Depends on: #107
Blocks: None

Scope

Import the authorized private PDF archives for 2024, 2025, and 2026 into the
bookkeeping document store delivered by #107. The migration must be an
operator-run script that defaults to dry-run, talks only to authenticated
bookkeeping APIs, is safe to resume, and proves that every accepted document and
relationship was reconciled. It must never use DynamoDB, S3 SDK, table, or bucket
credentials directly. A presigned upload obtained from the bookkeeping API is
part of the API upload lifecycle and is allowed.

The archives and all derived manifests/evidence are private operational data.
Keep them in their supplied local attachment location or project-local ignored
.tmp/; never commit, attach, or copy them into tracked/public paths. Public
console output, tests, issue comments, CI logs, and CloudWatch logs may contain
aggregate counts and reason codes only—not document names, paths, hashes,
contents, transaction values, contacts, account details, signed URLs, or other
finance context.

Safe archive inventory

Add a dedicated document importer under backend/scripts/ with an npm command.
It accepts one or more ZIP archives plus a private mapping manifest and defaults
to dry-run. Dry-run may use authenticated read-only bookkeeping APIs to resolve
accounts, transactions, existing documents, and links, but it performs no
upload, create, update, delete, report snapshot, or archive-generation call.

Inspect ZIP central-directory metadata before reading members and enforce
configurable conservative limits for archive bytes, member count, per-member
compressed and uncompressed bytes, total uncompressed bytes, and compression
ratio. Structural and resource safety validation applies to every member before
manifest dispositions are considered. Reject the entire run before writes for
an encrypted member, absolute or traversal path, NUL/control character,
symlink or other non-regular member, duplicate normalized member path,
malformed ZIP, or exceeded limit; a manifest exclusion can never waive or hide
one of these failures.

After that validation, the private manifest may explicitly mark a safe regular
non-PDF member or safe nested-archive member as excluded, with an allow-listed
reason. Excluded members are counted but never opened, extracted, recursively
inspected, hashed as import candidates, uploaded, or imported. An unsupported or
nested member without an explicit exclusion still fails closed before writes.
Ignore only documented packaging metadata entries. Accepted import members must
have a .pdf extension and valid %PDF- magic bytes; never trust extension or
archive MIME metadata alone. Stream/hash accepted PDFs without extracting them
to a shared filesystem path.

Compute SHA-256 locally for identity, then ask the API whether that content is
already active. Content identity, not filename or archive location, controls
deduplication. Repeated content within/between the three archives and content
already stored by #107 resolves to one document record. The API must enforce
hash uniqueness atomically under concurrent requests and return a safe
created/existing result; a duplicate must not leave a pending document or
orphan object. Idempotency keys must be deterministic and bounded. The API must
re-verify uploaded size, PDF magic, and SHA-256 before activation and clean up
failed/incomplete upload state.

Explicit mapping, statements, and links

Automatic OCR, PDF parsing, classification, account detection, and transaction
matching remain deferred as planned in #107. For this migration, a private
manifest supplies the operator-confirmed mapping. Identify each document in the
manifest by SHA-256 so renaming or moving an archive does not change identity.
Each mapping declares:

  • document type: invoice, receipt, bank statement, private-account statement,
    or other evidence;
  • statement month and account ID for every business/private statement;
  • zero or more existing transaction references using stable bookkeeping
    sourceKey values (transaction IDs may be accepted only as an explicit
    fallback and must be resolved through the API);
  • coverage type for each relationship (evidence or statement-coverage).

The importer must fail closed before writing when a manifest has duplicate
hash entries with conflicting metadata, an unknown account/transaction,
invalid type/month/coverage, an ambiguous transaction reference, a statement
without account/month, a non-statement with statement-only metadata, or a
non-import member without an explicit allowed exclusion. Documents intentionally
retained without a transaction link must be explicitly marked as such; omission
is not implicit approval. Exclusion entries identify an outer archive member
without exposing it publicly and include an allow-listed reason such as
unsupported-image or nested-packaging-archive; they cannot target an
accepted PDF or suppress a structural/resource validation error.

Create links through the API after document activation. One statement or other
PDF can cover multiple ledger transactions, and one transaction can link to
multiple PDFs. Link uniqueness is the tuple of document, transaction, and
coverage type; retries return the existing link and create no duplicate.
Business statements use the configured business account IDs and
statement-coverage; occasional private-account statements use an existing or
API-created private account, their statement month, and explicit covered
transactions. Do not infer coverage merely because a transaction falls in the
same month.

Controlled write, resume, reconciliation, and rollback

A write requires --write, an explicit target-environment confirmation, valid
portal/operator authentication, and a successful dry-run evidence file whose
archive/manifest fingerprints still match. The script must refuse a mismatched
or stale dry-run. Use bounded concurrency, retry only throttling/transient
failures with backoff, never retry permanent auth/validation failures blindly,
and checkpoint after each API result so an interrupted run resumes without
re-uploading or relinking completed work.

Assign every execution a non-sensitive migration run ID. Persist run ownership
and deterministic source provenance on newly created document/link metadata,
without exposing local paths. Pre-existing deduplicated documents/links are
recorded as reused, never claimed by the run. The private run evidence under
.tmp/bookkeeping-document-import/<run-id>/ includes inventory counts, rejected
reason codes, manifest fingerprint, API-created/reused IDs, link outcomes,
checkpoints, and reconciliation/rollback results. It may contain private detail
needed by the operator but must be ignored by Git and excluded from public logs.

After writes, query the authenticated API and reconcile exactly:

  • accepted unique hashes = newly created documents + reused active documents;
  • every accepted manifest entry resolves to one active document with matching
    size/hash/type/statement metadata;
  • requested unique links = newly created links + reused links;
  • every requested document/transaction/coverage tuple exists exactly once;
  • counts by archive year, document type, statement account/month, unlinked
    approval, explicit exclusion reason, rejected reason, and create/reuse outcome
    match the dry-run plan;
  • a second write run reports zero new documents and zero new links.

Any mismatch exits non-zero and does not claim success. Do not create immutable
monthly report snapshots as part of migration; reconciliation may use report
coverage/read APIs, while the operator creates a monthly package after reviewing
the imported evidence.

Provide API-only rollback for a named run with dry-run as the default and
--rollback --write plus target confirmation for mutation. It deletes only
links and documents created and owned by that run, links first, using
authenticated APIs. It must preserve every pre-existing/reused record, refuse
to delete a document referenced by a report or non-run link, delete the private
S3 object as well as metadata for eligible run-owned documents, tolerate an
already-rolled-back retry, and reconcile that no run-owned resources remain.
Rollback failures are checkpointed and reported without private values.

Acceptance Criteria

  • The importer defaults to no-write dry-run, accepts all three archive years
    plus a private manifest, uses authenticated bookkeeping API reads only during
    preview, and requires --write, matching environment confirmation, and a
    fingerprint-matching dry-run artifact before mutation.
  • ZIP handling rejects traversal/absolute paths, symlinks/non-regular
    entries, encrypted or malformed members, normalized-path collisions, invalid
    PDF magic, ZIP bombs, and configured count/size/ratio limits before any write.
    Only safe regular non-PDF or nested-archive members explicitly excluded by the
    private manifest may be skipped; they are never opened/extracted/imported,
    while unmanifested unsupported/nested members fail closed. Accepted PDFs are
    streamed rather than extracted into a shared path.
  • SHA-256 content identity deduplicates within one archive, across archive
    years, against existing active documents, and under concurrent upload
    requests. Duplicate/failed completion leaves no pending metadata or orphan
    object.
  • The document API supports an idempotent prepare/upload/complete lifecycle
    using existing portal/operator authentication, short-lived KMS-encrypted
    presigned PUTs, server-side size/magic/hash verification, bounded requests,
    safe responses, and no direct importer access to DynamoDB or S3 credentials.
  • Manifest validation is fail-closed and supports invoices, receipts,
    business statements for either configured account, occasional private-account
    statements, explicitly approved unlinked documents, and deterministic
    transaction resolution through API-visible sourceKey values.
  • API link creation is idempotent for the document/transaction/coverage
    tuple and supports one PDF covering multiple transactions plus multiple PDFs
    linked to one transaction. Statement coverage is explicit, not inferred by
    month.
  • Write execution uses bounded concurrency/backoff and durable private
    checkpoints; interruption and rerun resume safely without duplicate uploads,
    documents, links, or lost successful work.
  • Post-write reconciliation proves exact document and link equations,
    metadata/link correspondence, categorized inclusion/exclusion/rejection
    counts, and zero creates on a second run. A mismatch exits non-zero and
    retains private diagnostic evidence.
  • API-only rollback defaults to preview, removes only run-owned links and
    eligible run-owned document objects/metadata after explicit confirmation,
    preserves reused or externally referenced data, is retry-safe, and verifies
    the result.
  • Unauthorized document inventory, hash lookup, prepare, complete, link,
    rollback, and reconciliation requests are rejected without leaking whether a
    private hash, document, transaction, account, or link exists.
  • Public output and application logs contain only aggregate counts,
    non-sensitive run/credential IDs, and allow-listed reason codes. Signed URLs,
    document names/paths/hashes, PDF content, transaction values, contacts,
    account details, and manifest rows never enter GitHub, CI, or CloudWatch logs.
  • Automated tests use generated synthetic ZIPs/PDFs/manifests only and
    exercise real HTTP/Lambda route boundaries. Tests fail if the importer writes
    directly through storage clients.
  • The full backend workflow passes: cd backend && npm test,
    npm run typecheck, and npm run build. If SAM/IAM/environment configuration
    changes, also run sam validate --lint --template-file infra/template.full.yaml
    and sam build --template-file infra/template.full.yaml.
  • A tracked-file/privacy scan and git status confirm the supplied archives,
    manifests, fingerprints, checkpoints, PDFs, and reconciliation/rollback
    artifacts remain only in ignored/private locations.
  • [HUMAN] With the AWS sandbox gate open, an authorized operator reviews the
    private mapping manifest, runs dry-run then confirmed write for 2024–2026,
    retains private pre/post evidence, confirms exact reconciliation, spot-checks
    representative document/link/statement behavior in the authenticated portal,
    and confirms the second write creates zero documents and zero links.

Test Scenarios

Scenario: Dry-run plans without mutation

Given: Synthetic year archives, an explicit manifest, and authenticated API
fixtures containing transactions, accounts, documents, and links
When: The importer runs without --write
Then: It validates and reconciles a deterministic plan, makes no mutating API
call, and emits only aggregate public output and private ignored evidence

Scenario: Exclude supported packaging exceptions safely

Given: A synthetic archive has regular image and nested-archive members that
pass outer archive structural/resource checks
When: The private manifest explicitly excludes them with allowed reasons
Then: Dry-run counts the exclusions, never opens/extracts/imports them, and plans
only accepted PDFs; removing either exclusion makes the run fail closed

Scenario: Reject a hostile or oversized archive atomically

Given: ZIP fixtures containing traversal, symlink, encrypted, malformed,
invalid-PDF, duplicate-normalized-path, extreme-compression, and exceeded-limit
members, including variants also marked excluded in the manifest
When: Dry-run inventories each fixture
Then: It rejects the whole run before applying exclusions, returns allow-listed
reason codes, and performs no upload or metadata write

Scenario: Deduplicate content safely

Given: The same synthetic PDF appears under different names/years, already
exists remotely, or is prepared concurrently by two callers
When: The upload lifecycle runs or reruns
Then: Exactly one active document owns the hash, callers receive a safe
created/existing outcome, and no pending/orphan upload remains

Scenario: Model many-to-many evidence and statements

Given: A manifest where one statement covers several transactions, one
transaction has several evidence PDFs, both business accounts have statements,
and one private statement has explicit coverage
When: The confirmed import creates links
Then: Every requested tuple exists once with the declared account, month, type,
and coverage; no same-month relationship is inferred

Scenario: Resume after partial failure

Given: A confirmed write stops after some documents and links succeed
When: The same fingerprinted run resumes
Then: Checkpoints and API idempotency reuse completed work and create only the
missing resources

Scenario: Reject changed input after approval

Given: A successful dry-run evidence file
When: An archive or manifest changes before --write
Then: The importer refuses to mutate and requires a new dry-run

Scenario: Reconcile and prove a no-op rerun

Given: A completed migration
When: The importer queries API state and then repeats the confirmed write
Then: All document/link equations match and the rerun reports zero creates

Scenario: Roll back only run-owned data

Given: A run containing new and reused documents/links, with one run-owned
document referenced outside the run
When: Rollback is previewed, confirmed, interrupted, and retried
Then: Eligible run-owned links/objects/metadata are removed, reused and
externally referenced data are preserved, the refusal is explicit, and the
final reconciliation is accurate

Scenario: Deny unauthorized probing

Given: Missing or invalid operator authentication
When: A caller attempts hash lookup, upload, completion, linking, inventory, or
rollback
Then: The API returns an authorization error without disclosing resource
existence or private metadata

Out of Scope

  • OCR, PDF text extraction, automatic classification, candidate scoring, or
    automatic document-to-transaction/account matching.
  • Parsing bank-statement lines or creating/updating ledger transactions from
    PDFs; the already imported bookkeeping ledger remains authoritative.
  • Creating historical monthly report snapshots/ZIPs automatically during this
    migration, emailing an accountant, or exposing public/permanent download
    links.
  • Importing or recursively inspecting non-PDF attachments or nested archives.
    Safe regular members of those types may only be explicitly excluded; encrypted
    archives, corrupted documents, unsafe members, and files without explicit
    manifest disposition remain rejected.
  • Changing or deleting the source Google Sheet/workbook or the supplied source
    archives.
  • Changes to source repositories outside DataTalksClub/dataops or adding
    operational documents to this public repository.

Dependencies

  • Add private bookkeeping ledger and migrate the historical workbook into AWS sandbox #107 is deployed and provides the private bookkeeping table, document bucket,
    accounts, authenticated API, many-to-many links, and monthly package flow.
  • Real archives, mapping manifests, dry-run approvals, checkpoints,
    reconciliation outputs, and rollback evidence are private operator artifacts
    and must remain in ignored .tmp/ or the separate private knowledge boundary.
  • The authorized aggregate source baseline is 445 top-level PDFs, three
    top-level non-PDF images, and two top-level nested packaging archives. Eight
    top-level PDF copies are content duplicates, leaving 437 unique top-level PDFs.
    The nested packaging archives contain 28 PDFs already byte-identical to
    top-level PDFs; those archives are explicitly excluded and never
    extracted/imported. Dry-run and post-write reconciliation must account for
    these aggregate inclusion, deduplication, and exclusion counts exactly.
  • The existing create-only transaction ingestion credential intentionally has
    no file permissions. This migration uses portal/operator authentication for
    document APIs; it must not broaden the webhook credential into document read,
    download, delete, report, or admin access.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1ImportantbackendBackend/APIdataData model, migration, storageenhancementNew or improved functionalityhumanCode done or issue blocked on human verificationmigrationImport or migration worktestingTests and QA

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions