# Planogrammer Review Guidelines These guidelines are derived from real code review comments on Planogrammer merge requests. Higher-numbered MRs (newer) take precedence where guidelines conflict. --- ## Code Style & Naming - **DO** name variables to reflect their precise meaning. Prefer `non_existing_ids` over `missing`, `changed_column_ids` over `changes`. The name should answer "what is this a collection of?" (MR 134) - **DO** name boolean variables so they read as a statement: ```python is_changed = column.id in changed_column_ids ``` Align the name with the underlying table/entity where it helps (`machine_column_changes` -> `is_changed`). (MR 134) - **DO** simplify boolean assignments. Instead of an `if/else` block that sets `True`/`False`: ```python # Before if source == PlanogramChangeRequestSource.AI: machine.planogram_optimized_by_ai = True else: machine.planogram_optimized_by_ai = False # After machine.planogram_optimized_by_ai = source == PlanogramChangeRequestSource.AI ``` (MR 118) - **DO** use the `X | None` syntax for optional type hints: ```python column_filter: PushPlanogramColumnFilterDto | None = None ``` (MR 134) - **DO** prefix limit/capacity constants with `MAX_`, e.g. `MAX_GET_PRODUCT_BULK_IDS = 1000`. This signals that the constant is an upper bound, not a config value. (MR 94) - **DO** suffix all custom exception classes with `Exception`, e.g. `UnableToDeleteColumnException`, `BulkLimitExceededException`. (MR 65) - **DO** use single `_` prefix for protected helpers, double `__` prefix for private module-level functions that implement name mangling. The convention is: `_` = protected, `__` = private. (MR 52) - **DO** prefer named-constant arithmetic for time durations: ```python SHORT_PRICE_REVERT_PERIOD = 24 * HOURS # readable # not: 86400 # magic number # not: 60 * 60 * 24 # still unclear at a glance ``` (MR 41) - **DO** extract repeated small operations into named helper functions: ```python def _clear_changed_prices(column): ... def _get_price_or_none(price): ... ``` This removes duplication and makes the calling code read at a consistent level of abstraction. (MR 134) - **DO** decompose complex boolean comparisons into named intermediate variables: ```python are_products_equal = layout_column.product is None or layout_column.product == machine_column.product are_recipes_equal = layout_column.recipe is None or layout_column.recipe == machine_column.recipe return are_products_equal and are_recipes_equal and ... ``` (MR 61) - **DO** invert conditionals to enable early-return/continue patterns. Instead of a large `if is_changed` block: ```python if not is_changed: _clear_changed_prices(column) continue # handle the positive case at normal indentation ``` (MR 134) - **AVOID** using `assert` in production/runtime code. Assertions are stripped with `-O` and communicate the wrong intent. Raise an explicit exception instead. (MRs 61, 65) - **AVOID** a static method when a module-level constant object suffices. Static methods add unnecessary call overhead and obscure intent. (MR 119) - **AVOID** using `api` in endpoint paths -- all paths are API paths by definition. Similarly, avoid `internals` as a path segment if the endpoint belongs in an existing domain hierarchy. (MRs 53, 52) - **AVOID** tabs; use spaces. Run `ruff format` before pushing. Install pre-commit hooks so formatting is enforced automatically on every commit. (MRs 52, 94) --- ## API Design - **DO** place GET endpoints for a new domain in their own folder, not in an existing domain's folder. For example, `planogram_change_requests/` gets its own `endpoints.py`, `manager.py`, `dto.py`, and `exceptions.py`. Don't put PCR read endpoints inside `push/`. (MR 134) - **DO** add an upper-bound validation on all bulk list inputs. If a caller sends more than the allowed maximum (e.g. 1000 IDs), raise an exception immediately rather than letting the query run. Name the limit constant with a `MAX_` prefix: ```python MAX_GET_PRODUCT_BULK_IDS = 1000 if len(ids) > MAX_GET_PRODUCT_BULK_IDS: raise BulkLimitExceededException(...) ``` (MRs 139, 94) - **DO** make `source` a mandatory query parameter when it is always required for business logic. Optional parameters that are always supplied create false API flexibility. (MR 107) - **DO** consider adding optional boolean flags for batch callers who pre-validate: ```python check_machine_in_route: bool = Query(default=True) ``` This lets callers with their own validation skip redundant checks without breaking the default behavior. (MR 97) - **DO** use `company_exists(company_id)` when you only need to verify existence; don't fetch the full company ORM object just to check it's not `None`. Reserve `get_company` for when you need the object's fields. (MR 94) - **DO** for POST/PUT/PATCH/DELETE endpoints, instantiate the repository directly in the endpoint function rather than injecting it as a FastAPI dependency. This ensures that if an exception is raised partway through the handler, the transaction rolls back correctly. (MR 61) - **DO** use the correct HTTP status code for business-logic conflicts: `HTTP_409_CONFLICT` (not `HTTP_403_FORBIDDEN`) when the conflict is unrelated to authorization. (MR 118) - **DO** treat `ALREADY_SEEN` responses from dmsync as a success (2xx), not an error. Only raise when the response indicates a genuine failure. (MR 119) - **DO** use `RouteServiceAPIError` from `televend-core`, not from `cloud-adapters`. Always prefer the `televend-core` version of shared error classes. (MR 130) - **DO** write query parameter help text in imperative form: `Set this parameter to "true" in order to load brands and models.` Don't expose internal implementation details (e.g. which database is queried) in OpenAPI docs. (MRs 18, 19) - **DO** keep success responses for void operations minimal. An endpoint that performs an action and has nothing meaningful to return should respond with a short `"ok"` or an empty 204, not a verbose success DTO. (MRs 61, 65) - **DO** use mutually exclusive validation (XOR) on DTO fields when exactly one of several filters must be provided: ```python @model_validator(mode="after") def validate_exclusive_filter(self) -> "PushFilterDto": provided = sum([bool(self.ids), bool(self.columns), bool(self.view_columns)]) if provided != 1: raise ValueError("Exactly one filter must be provided") return self ``` (MR 134) - **AVOID** kebab-case query parameter names when the rest of the service uses snake_case. Mixing casing conventions breaks uniformity and can create subtle bugs. (MR 22) --- ## Error Handling & Exceptions - **DO** place domain-specific exceptions in that domain's own `exceptions.py`. If an exception is only raised and caught within the `push/` flow, it belongs in `service/api/.../push/exceptions.py`, not in a shared module. (MR 139) - **DO** add a catch-all handler in every endpoint for the base exception class of your domain exceptions. This ensures that any exception you forgot to handle individually still returns a structured error response rather than a 500. (MRs 61, 65) - **DO** log the caught exception (at least at `DEBUG` level) when swallowing or transforming it, so we can trace the original cause in logs without propagating it to the caller. (MRs 52, 61) - **DO** import only the specific exception class you need, not the entire `exceptions` module, to keep imports explicit and avoid namespace pollution. (MR 65) - **AVOID** raising a generic `Exception` for domain-specific error conditions. Define a named exception class that describes what went wrong. (MR 65) --- ## Testing - **DO** assert on IDs only when verifying list/collection responses, not on the full DTO content: ```python assert {r["id"] for r in response_json["content"]} == {p1.id, p2.id} ``` This prevents test breakage when DTO fields are added or renamed. (MR 94) - **DO** write test cases for PCR-type filtering correctness. A `PRICE_CHANGE` PCR must not create `MachineColumnChange` records for columns that only have structural (column) changes, and vice versa. Cover all three types: `PRICE_CHANGE`, `COLUMN_CHANGE`, `PLANOGRAM_CHANGE`, including partial/filtered scenarios. (MR 134) - **DO** add tests for prices with 4 decimal places. This edge case has caused production bugs and must be covered explicitly. (MR 65) - **DO** write a serialization test for every response DTO that contains `Decimal` or `condecimal` fields: ```python def test_when_model_dump_with_json_mode_then_price_is_float(): dto = MyResponseDto(price=Decimal("1.2345")) result = json.loads(dto.model_dump_json()) assert isinstance(result["price"], float) ``` (MR 122) - **DO** use standard pytest `setup_method` / `teardown_method` for synchronous setup/teardown. Only use `async` fixtures when you genuinely need to `await` inside them. (MR 52) - **AVOID** hard-coding specific auto-increment IDs in test assertions. Use the IDs from the objects you created in the test setup, not literal integers. (MR 94) --- ## Architecture & Domain Rules - **DO** fetch company/entity once per request and pass the ORM object to helper functions. Do not call `get_company` multiple times in the same request path. (MR 139) - **DO** move entity existence checks into manager/service methods, not into the endpoint layer. The endpoint should call `manager.do_something(company, ids)` and let the manager validate that all IDs exist. (MR 139) - **DO** batch multiple individual queries into a single `list` query where possible, and wrap the entire batch in a single `try/except` block. (MR 139) - **DO** decompose large methods that do multiple conceptually distinct things. A method that detects the PCR type and also builds the column dict should be split: one method returns the type, a separate method builds the dict. (MR 134) - **DO** keep manager responsibilities focused on a single flow. The `PushManager` handles the push flow. Read/GET logic for `PlanogramChangeRequest` belongs in a separate `PCRManager` or equivalent. Don't put GET logic in the push manager. (MR 134) - **DO** initialize collaborating managers in the constructor of the owning manager, not lazily inside methods. (MR 134) - **DO** filter `MachineColumnChange` records by PCR type. A `PRICE_CHANGE` PCR should only create MCCs for columns with pending price changes; a `COLUMN_CHANGE` PCR only for structural column changes. Validate this in tests. (MR 134) - **DO** use `utcnow()` from the project's utility module instead of `datetime.utcnow()` directly. This keeps timestamp generation consistent and easy to mock in tests. (MR 138) - **DO** prefer a direct field-update query over a fetch-then-modify-then-save pattern when you are only updating a single field. This avoids a redundant round trip. (MR 138) - **DO** use `save_many(entities)` for bulk saves instead of looping over individual `save(entity)` calls. (MRs 61, 65) - **DO** pass already-loaded ORM objects to methods rather than re-fetching by ID. If you have the object in scope, use it. (MR 61) - **DO** prefer explicit `save()` calls over relying on implicit ORM dirty-tracking. Explicit saves make the persistence boundary visible and prevent subtle bugs when objects are shared across transactions. (MR 118) - **AVOID** nesting context managers from different sources (adapter context manager wrapping a core context manager). Use one transaction boundary per operation. (MR 134) - **AVOID** the deprecated `load_options` parameter on repository calls. Define typed `LoadOptions` classes in `televend-core` and reference those instead. (MRs 61, 65) - **AVOID** duplicate DB fetches for the same entity in the same request. Fetch once, reuse the result. (MRs 139, 94) - **AVOID** generalizing too early. If two similar APIs share code only by coincidence and would diverge with the next requirement, keep them separate (Rule of Three). Document when you intentionally choose not to reuse an existing method. (MR 53) - **AVOID** calling the scheduler's `run_immediately` flag unnecessarily. Running tasks immediately on every API call bypasses the scheduler's batching and can overload downstream services. (MR 50) --- ## DTO & Serialization - **DO** implement `from_model(cls, orm_model)` as a classmethod on every response DTO. Keep endpoint handlers clean; don't inline model-to-DTO mapping there: ```python @classmethod def from_model(cls, m: MachinePCR) -> "MachinePCRDto": return cls(id=m.id, status=m.status, ...) ``` (MR 134) - **DO** use `model_dump_json()` (not `model_dump()`) when serializing DTOs that contain `Decimal` or `datetime` fields, to ensure correct JSON types (float, ISO string) rather than Python object types. (MR 95) - **DO** use `ConfigDict` consistently in all Pydantic v2 models. Don't mix old `class Config` style with the new `model_config = ConfigDict(...)` style in the same codebase. (MR 95) - **DO** add `json_encoders` in `ConfigDict` for any `datetime` fields that require custom serialization behavior across all relevant models. (MR 95) - **DO** give optional Pydantic fields an explicit `default=None`: ```python class MyDto(BaseModel): filter: str | None = None ``` (MR 95) - **DO** use `[[tool.uv.index]]` in `pyproject.toml` for configuring private package index sources, not under pytest options or other tool sections. (MR 95) --- ## Git & Process - **DO** name feature branches `feature/CLOUD-NNNNN-short-description`. The `feature/` prefix is required for CI/CD pipeline triggers. Branch names without this prefix will not run the build pipeline. (MR 21) - **DO** include the full endpoint path in CHANGELOG entries and make the description meaningful: - Bad: `Added new endpoint` - Good: `POST /v1/tenants/{tenant_id}/companies/{company_id}/planogram-change-requests/push/bulk -- creates PCRs in bulk for given machine IDs` Follow the CHANGELOG wiki format documented in Confluence. (MRs 65, 94, 52) - **DO** rebase your feature branch on `develop` before requesting review to avoid merge conflicts that reviewers then have to mentally skip. (MR 134) - **DO** install and run pre-commit with ruff so that formatting issues are caught before push, not during review. (MR 94) - **AVOID** `[ci-skip]` commits for changelog-only changes unless the change truly has no code impact; prefer including the changelog update in the feature commit. (MR 52)