# BohReport

Library that provides BOH report logic as a reusable package, avoiding duplication across projects.

## Installation

```toml
# pyproject.toml
dependencies = ["BohReport>=1.0"]

[[tool.poetry.source]]
name = "E-Deploy"
url = "https://pip.e-deploy.com.br"
priority = "supplemental"
```

## Namespace `boh`

`boh` é um *implicit namespace package* (PEP 420) — o diretório `src/boh/` não tem `__init__.py`. Isso permite que múltiplos pacotes (`boh.report`, `boh.utils`, etc.) coexistam sem conflito quando instalados juntos.

**Regra:** nunca crie `boh/__init__.py`. Lógica, constantes e versões ficam dentro de `boh/report/`, nunca no raiz do namespace.

## Concepts

| Concept | Responsibility |
|---|---|
| `ReportProcessor` | Receives the event, parses the payload, calls the interactor, dispatches the response |
| `CustomRenderReportResponse` | Builds the report structure: resources, renderers and preparers |
| `Renderer` | Defines how data is displayed (table, sheet, grouped) |
| `Preparer` | Transforms or configures data before rendering (bold, format, group, exclude) |
| `Sender` | Dispatches the response via MessageBus — inline or uploading to S3 |

## Creating a report processor

Extend `ReportProcessor` and implement `parse_data`. Override `call_business` only if the default (`interactor.execute(request)`) is not enough.

```python
from boh.report.processor import ReportProcessor

class FetchInventoryReportProcessor(ReportProcessor):
    def __init__(self, interactor: FetchInventoryReportInteractor, logger: Logger):
        super().__init__(
            report_name="Inventory",
            parser=self._parse,
            logger=logger,
            interactor=interactor,
        )

    def _parse(self, data: dict, event_type: str) -> FetchInventoryReportRequest:
        return FetchInventoryReportRequest(
            report_id=data["report-id"],
            store_id=data["store-id"],
            start_dt=data["data"]["start-dt"],
            end_dt=data["data"]["end-dt"],
        )
```

## Building the report response

The interactor builds a `CustomRenderReportResponse` with resources, preparers and renderers.

```python
from boh.report.layout import (
    CustomRenderReportResponse,
    CustomReportConditions,
    CustomReportNamedResource,
    ReportTitle,
    ReportValue,
    ReportValueFormatters,
)
from boh.report.layout.renderer import SheetTableRenderer, TableRenderer, GroupTableRenderer
from boh.report.layout.preparer import (
    AddBoldStylePreparer,
    DisableXlsLayoutFieldsPreparer,
    GroupDataPreparer,
    ConfigureSheetTablesPreparer,
)

class FetchInventoryReportInteractor:
    def execute(self, request: FetchInventoryReportRequest) -> CustomRenderReportResponse:
        response = CustomRenderReportResponse()

        # Source data metadata
        response.add_source_data(
            title=ReportTitle("$GENERATED_ON_TEXT"),
            value=ReportValue(value=datetime.utcnow().isoformat(), formatter=ReportValueFormatters.DATETIME),
        )

        # Filters shown in the report header
        response.add_filter(
            ReportTitle("$COMMONS_FILTER_BUSINESS_DATE_RANGE", [str(request.start_dt), str(request.end_dt)])
        )

        # Column definitions with formatters
        response.add_resource("columns", [
            {"header": "$ITEM_CODE",        "id": "code"},
            {"header": "$ITEM_DESCRIPTION", "id": "description"},
            {"header": "$QUANTITY",         "id": "qty",   "format": ReportValueFormatters.NUMBER},
            {"header": "$UNIT_COST",        "id": "cost",  "format": ReportValueFormatters.CURRENCY},
            {"header": "$DATE",             "id": "date",  "format": ReportValueFormatters.DATE},
        ])

        # Row data
        response.add_resource("data", self._fetch_rows(request))

        # Preparers
        response.add_preparer(DisableXlsLayoutFieldsPreparer())
        response.add_preparer(
            AddBoldStylePreparer(
                fields=["code", "description", "qty", "cost"],
                resource=CustomReportNamedResource(name="columns", type="resources"),
                conditions=[CustomReportConditions.IS_PDF],
            )
        )

        # Renderers
        response.add_renderer(
            TableRenderer(
                columns=CustomReportNamedResource(name="columns", type="resources"),
                data=CustomReportNamedResource(name="data", type="resources"),
                auto_filter=True,
                conditions=[CustomReportConditions.IS_PDF],
            )
        )
        response.add_renderer(
            SheetTableRenderer(
                columns=CustomReportNamedResource(name="columns", type="resources"),
                data=CustomReportNamedResource(name="data", type="resources"),
                auto_filter=True,
                auto_fit_width=True,
                conditions=[CustomReportConditions.IS_XLS],
            )
        )

        return response
```

## Multiple sheets (XLS)

Use `ConfigureSheetTablesPreparer` together with multiple `SheetTableRenderer`, one per sheet.

```python
response.add_preparer(ConfigureSheetTablesPreparer())

for store in stores:
    response.add_resource(f"{store.code}_columns", [...])
    response.add_resource(f"{store.code}_data", self._fetch_rows(store))
    response.add_renderer(
        SheetTableRenderer(
            columns=CustomReportNamedResource(name=f"{store.code}_columns", type="resources"),
            data=CustomReportNamedResource(name=f"{store.code}_data", type="resources"),
            sheet_name=store.description,
            auto_fit_width=True,
            conditions=[CustomReportConditions.IS_XLS, CustomReportConditions.CONFIGURE_SHEET_TABLES],
        )
    )
```

## Grouped tables (PDF)

Use `GroupDataPreparer` to group rows and `GroupTableRenderer` to render one sub-table per group.

```python
response.add_preparer(
    GroupDataPreparer(
        fields=["shift"],
        group_name="grouped_by_shift",
        resource=CustomReportNamedResource("data", "resources"),
        conditions=[CustomReportConditions.IS_PDF],
    )
)
response.add_renderer(
    GroupTableRenderer(
        columns=CustomReportNamedResource("columns", "resources"),
        group_name="grouped_by_shift",
        group_title="shift",
        group_spacing=5,
        conditions=[CustomReportConditions.IS_PDF],
    )
)
```

## Senders

`S3Sender` (default) uploads the response to the URL in `response-upload-url` and falls back to inline when absent. Use `BaseSender` when S3 is not needed.

```python
from boh.report.sender import BaseSender, S3Sender

# Override in the processor constructor only when you need custom behavior
processor = FetchInventoryReportProcessor(
    interactor=interactor,
    logger=logger,
    sender=BaseSender(),  # skip S3
)
```