Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .ai-context/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ bb status
bb documents import --dry-run --file path/to/document.pdf
bb documents import --file path/to/document.pdf
bb documents list
bb documents list --type bank_statement --jurisdiction US --tax-year 2025
bb documents show 1
bb documents update 1 --type bank_statement --jurisdiction US --tax-year 2025
```

`bb` is the side-by-side command surface for new `BB_` schema work. It should
Expand All @@ -49,6 +51,8 @@ prepare v2 `financial/` storage roots for the active data home.
document/object metadata, and copies the canonical object into managed storage.
Use `bb documents list` and `bb documents show DOCUMENT_ID` to inspect imported
v2 documents without opening SQLite directly.
Use `bb documents update DOCUMENT_ID` to set document metadata. `bb documents
list` supports metadata filters for type, jurisdiction, tax year, and status.

## BankBuddy CLI

Expand Down
5 changes: 4 additions & 1 deletion .ai-context/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ section is `Unreleased`.
transactions, categories, reports, exports, storage migration, and status.
- Side-by-side `bb` CLI for v2 financial intelligence initialization,
foundation status, storage readiness, generic document import, document
inventory inspection, and `BB_` schema visibility.
inventory inspection, metadata edits, metadata filters, and `BB_` schema
visibility.
- Supported banking imports for Bank of America PDF/CSV, Apple Card PDF, ICICI
`.xls`, and HDFC `.xls`.
- Statement inventory and statement coverage auditing.
Expand All @@ -37,6 +38,8 @@ section is `Unreleased`.
`BB_DOCUMENT` and `BB_DOCUMENT_OBJECT` with SHA-256 canonical storage keys.
- `bb documents list/show` for read-only inspection of generic v2 document and
canonical object metadata.
- `bb documents update` and metadata filters on `bb documents list` for manual
classification by type, jurisdiction, tax year, and status.
- Prospective relicensing from MIT to AGPL-3.0-or-later.
- Canonical data-home layout with `database/`, `bank/`, and `tax/` directories.
- First TaxBuddy CLI slice and `tax_documents` metadata index.
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ and versions are tracked in the repo-root `VERSION` file.

### Added

- Added `bb documents update DOCUMENT_ID` for v2 document metadata edits and
metadata filters on `bb documents list`.
- Added `bb documents list` and `bb documents show DOCUMENT_ID` for read-only
inspection of generic v2 document records and canonical object metadata.
- Added `bb documents import --dry-run --file ...` and
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ bb status
bb documents import --dry-run --file path/to/document.pdf
bb documents import --file path/to/document.pdf
bb documents list
bb documents list --type bank_statement --jurisdiction US --tax-year 2025
bb documents show 1
bb documents update 1 --type bank_statement --jurisdiction US --tax-year 2025
bankbuddy --help
bankbuddy status
bankbuddy init
Expand Down Expand Up @@ -174,6 +176,10 @@ file, records a `BB_DOCUMENT`, stores a canonical object under
workflows.
Use `bb documents list` and `bb documents show DOCUMENT_ID` to inspect the
generic v2 document inventory and canonical object metadata.
Use `bb documents update DOCUMENT_ID` to classify imported generic documents
with metadata such as type, jurisdiction, tax year, and status. `bb documents
list` accepts the same metadata filters so you can inspect a focused document
set without opening SQLite.

Switch the current shell by exporting `BANKBUDDY_ENV`:

Expand Down
94 changes: 90 additions & 4 deletions src/bankbuddy/bb/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@

from bankbuddy import __version__
from bankbuddy.database import initialize_database
from bankbuddy.bb.documents import DOCUMENT_STATUSES
from bankbuddy.bb.documents import DocumentImportError
from bankbuddy.bb.documents import DocumentMetadataError
from bankbuddy.bb.documents import DocumentSummary
from bankbuddy.bb.documents import get_document_summary
from bankbuddy.bb.documents import import_document
from bankbuddy.bb.documents import list_documents
from bankbuddy.bb.documents import plan_document_import
from bankbuddy.bb.documents import update_document_metadata
from bankbuddy.paths import resolve_app_paths
from bankbuddy.bb.storage import ensure_financial_storage_dirs
from bankbuddy.runtime import CliRuntime
Expand Down Expand Up @@ -173,13 +176,38 @@ def documents() -> None:


@documents.command("list")
@click.option("--type", "document_type", help="Filter by document type.")
@click.option("--jurisdiction", "jurisdiction_code", help="Filter by jurisdiction code.")
@click.option(
"--tax-year",
type=click.IntRange(1000, 9999),
help="Filter by four-digit tax year.",
)
@click.option(
"--status",
"document_status",
type=click.Choice(DOCUMENT_STATUSES),
help="Filter by document status.",
)
@click.pass_context
def documents_list(ctx: click.Context) -> None:
def documents_list(
ctx: click.Context,
document_type: str | None,
jurisdiction_code: str | None,
tax_year: int | None,
document_status: str | None,
) -> None:
"""List imported v2 documents."""

runtime = runtime_from_context(ctx)
paths = resolve_app_paths(environment=runtime.environment)
rows = list_documents(paths)
rows = list_documents(
paths,
document_type=document_type,
jurisdiction_code=jurisdiction_code,
tax_year=tax_year,
document_status=document_status,
)
render_document_table(rows)


Expand All @@ -197,6 +225,51 @@ def documents_show(ctx: click.Context, document_id: int) -> None:
render_document_summary(summary)


@documents.command("update")
@click.argument("document_id", type=int)
@click.option("--type", "document_type", help="Set the document type.")
@click.option("--jurisdiction", "jurisdiction_code", help="Set the jurisdiction code.")
@click.option(
"--tax-year",
type=click.IntRange(1000, 9999),
help="Set the four-digit tax year.",
)
@click.option(
"--status",
"document_status",
type=click.Choice(DOCUMENT_STATUSES),
help="Set the document status.",
)
@click.pass_context
def documents_update(
ctx: click.Context,
document_id: int,
document_type: str | None,
jurisdiction_code: str | None,
tax_year: int | None,
document_status: str | None,
) -> None:
"""Update one v2 document's metadata."""

runtime = runtime_from_context(ctx)
paths = resolve_app_paths(environment=runtime.environment)
try:
summary = update_document_metadata(
paths,
document_id,
document_type=document_type,
jurisdiction_code=jurisdiction_code,
tax_year=tax_year,
document_status=document_status,
)
except DocumentMetadataError as exc:
raise click.ClickException(str(exc)) from exc
if summary is None:
raise click.ClickException(f"Document not found: {document_id}")

render_document_summary(summary)


@documents.command("import")
@click.option("--dry-run", is_flag=True, help="Plan the import without writes.")
@click.option(
Expand Down Expand Up @@ -265,6 +338,9 @@ def render_document_table(rows: list[DocumentSummary]) -> None:
[
str(row.document.document_id),
row.document.original_file_name,
_display_value(row.document.document_type),
_display_value(row.document.jurisdiction_code),
_display_value(row.document.tax_year),
row.document.document_status,
str(row.canonical_object.byte_size)
if row.canonical_object and row.canonical_object.byte_size is not None
Expand All @@ -275,9 +351,19 @@ def render_document_table(rows: list[DocumentSummary]) -> None:
for row in rows
]
render_pretty_table(
["ID", "File", "Status", "Size", "Media Type", "SHA-256"],
[
"ID",
"File",
"Type",
"Jurisdiction",
"Tax Year",
"Status",
"Size",
"Media Type",
"SHA-256",
],
table,
align_right={0, 3},
align_right={0, 4, 6},
)


Expand Down
83 changes: 80 additions & 3 deletions src/bankbuddy/bb/dao.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

from bankbuddy.bb.records import (
DocumentCreate,
DocumentListFilter,
DocumentMetadataUpdate,
DocumentRecord,
EntityAttributeCreate,
EntityAttributeRecord,
Expand Down Expand Up @@ -186,11 +188,32 @@ def get_document(self, document_id: int) -> DocumentRecord | None:
return None
return _document_from_row(row)

def list_documents(self) -> list[DocumentRecord]:
def list_documents(
self,
filters: DocumentListFilter | None = None,
) -> list[DocumentRecord]:
"""Return v2 documents ordered by id."""

filters = filters or DocumentListFilter()
conditions: list[str] = []
params: list[object] = []
if filters.document_type is not None:
conditions.append("document_type = ?")
params.append(filters.document_type)
if filters.jurisdiction_code is not None:
conditions.append("jurisdiction_code = ?")
params.append(filters.jurisdiction_code)
if filters.tax_year is not None:
conditions.append("tax_year = ?")
params.append(filters.tax_year)
if filters.document_status is not None:
conditions.append("document_status = ?")
params.append(filters.document_status)
where_clause = ""
if conditions:
where_clause = "where " + " and ".join(conditions)
rows = self._conn.execute(
"""
f"""
select
document_id,
file_hash,
Expand All @@ -202,11 +225,65 @@ def list_documents(self) -> list[DocumentRecord]:
tax_year,
document_status
from BB_DOCUMENT
{where_clause}
order by document_id
"""
""",
params,
).fetchall()
return [_document_from_row(row) for row in rows]

def update_document_metadata(
self,
document_id: int,
update: DocumentMetadataUpdate,
) -> DocumentRecord | None:
"""Update metadata fields on one v2 document."""

assignments: list[str] = []
params: list[object] = []
if update.document_type is not None:
assignments.append("document_type = ?")
params.append(update.document_type)
if update.jurisdiction_code is not None:
assignments.append("jurisdiction_code = ?")
params.append(update.jurisdiction_code)
if update.tax_year is not None:
assignments.append("tax_year = ?")
params.append(update.tax_year)
if update.document_status is not None:
assignments.append("document_status = ?")
params.append(update.document_status)
if not assignments:
return self.get_document(document_id)

params.append(document_id)
cursor = self._conn.execute(
f"""
update BB_DOCUMENT
set
{", ".join(assignments)},
updated_at = current_timestamp
where document_id = ?
""",
params,
)
if cursor.rowcount == 0:
return None
return self.get_document(document_id)

def jurisdiction_exists(self, jurisdiction_code: str) -> bool:
"""Return whether a seeded jurisdiction exists."""

row = self._conn.execute(
"""
select 1
from BB_JURISDICTION
where jurisdiction_code = ?
""",
(jurisdiction_code,),
).fetchone()
return row is not None

def create_entity(self, record: EntityCreate) -> EntityRecord:
"""Create a v2 entity row."""

Expand Down
Loading
Loading