first commit
This commit is contained in:
175
REVIEW_GUIDELINES_assetor.md
Normal file
175
REVIEW_GUIDELINES_assetor.md
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
# Assetor Review Guidelines
|
||||||
|
|
||||||
|
Guidelines derived from 28 merged MRs of real code review history. Newer reviews take precedence.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Style & Naming
|
||||||
|
|
||||||
|
- **AVOID** returning multi-value tuples from methods when a named dataclass would be clearer.
|
||||||
|
Tuple destructuring at the call site is hard to read and fragile.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# AVOID
|
||||||
|
(asset_per_device_id, company_brand_ids, failed_responses) = await manager.bulk_create_preload(body, user_id)
|
||||||
|
|
||||||
|
# DO instead
|
||||||
|
bulk_preload: BulkPreloadDto = await manager.bulk_create_preload(body, user_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `@dataclass` (not Pydantic) for internal result objects to avoid forward-ref issues.
|
||||||
|
Pair with `from __future__ import annotations` and `TYPE_CHECKING` guards for
|
||||||
|
circular-import-safe type hints. (MR !3)
|
||||||
|
|
||||||
|
- **DO** use typed dataclasses for structured error/response payloads rather than plain dicts.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# AVOID
|
||||||
|
failed_responses.append({"device_id": device.id, "reason": "..."})
|
||||||
|
|
||||||
|
# DO instead
|
||||||
|
@dataclass
|
||||||
|
class FailedResponse:
|
||||||
|
device_id: int
|
||||||
|
reason: str
|
||||||
|
|
||||||
|
# Consider static factory methods for repeated reason strings
|
||||||
|
FailedResponse.for_device_already_has_asset(device_id)
|
||||||
|
```
|
||||||
|
(MR !3)
|
||||||
|
|
||||||
|
- **DO** use `Iterable[T]` for input parameters that only need to be iterated, not `List[T]`.
|
||||||
|
Accept the most general type; return the most specific type. (MR !3)
|
||||||
|
|
||||||
|
- **DO** name filter/query variables accurately. A misspelled variable like `asset_item_filer`
|
||||||
|
must be corrected before merge. (MR !21)
|
||||||
|
|
||||||
|
- **DO** order method parameters hierarchically: `tenant_id` before `company_id`, more general
|
||||||
|
identifiers before more specific ones. Apply consistently across all call sites and signatures.
|
||||||
|
(MR !21)
|
||||||
|
|
||||||
|
- **AVOID** `datetime.utcnow()` in new code. Use `datetime.now(UTC)` —
|
||||||
|
`utcnow()` is deprecated in Python 3.12+. (MR !25)
|
||||||
|
|
||||||
|
- **AVOID** broad ruff/mypy ignore rules in `pyproject.toml`. Do not suppress entire error
|
||||||
|
categories without confirming they are genuinely needed — run `pre-commit run --all-files`
|
||||||
|
to verify. (MR !25)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Design
|
||||||
|
|
||||||
|
- **DO** return `404` (not `400` or `500`) when a referenced object is not found on POST and
|
||||||
|
PATCH endpoints. (MR !13)
|
||||||
|
|
||||||
|
- **DO** keep `_update_fields_from_dto()` methods focused. Extract multi-step sub-operations
|
||||||
|
(e.g. warehouse update, master_system_id update) into their own dedicated `_update_<field>()`
|
||||||
|
private methods rather than embedding logic inline. (MR !21)
|
||||||
|
|
||||||
|
- **DO** use the correct auth client factory for each context:
|
||||||
|
- User token required: `AuthServiceAPIClientFactory.create_user_client(...)` → `AuthorizationServiceAPIClient`
|
||||||
|
- No authorization: `AuthServiceAPIClientFactory.create_public_client(...)` → `AuthorizationServicePublicAPIClient`
|
||||||
|
|
||||||
|
Do not mix up the two client types. (MR !29)
|
||||||
|
|
||||||
|
- **DO** use SQLAlchemy 2.x style in all new queries:
|
||||||
|
- `select(Model)` not `select([Model])`
|
||||||
|
- `(condition, value)` not `[(condition, value)]` in CASE expressions
|
||||||
|
- Add `.mappings()` to raw SQL results for dictionary-like access
|
||||||
|
(MR !29)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling & Exceptions
|
||||||
|
|
||||||
|
- **DO** narrow `try/except` blocks to only the line(s) that can raise the caught exception.
|
||||||
|
Keep all subsequent logic outside the `try` block.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# AVOID
|
||||||
|
try:
|
||||||
|
assignment = await self._get_asset_machine(asset)
|
||||||
|
if assignment.machine_id is None:
|
||||||
|
raise AssetItemNotAssigned(asset.id)
|
||||||
|
if machine.id != assignment.machine_id:
|
||||||
|
raise AssetItemAssignedToDifferentMachine(asset.id)
|
||||||
|
except ObjectNotFound:
|
||||||
|
raise AssetItemNotAssigned(asset.id)
|
||||||
|
|
||||||
|
# DO instead
|
||||||
|
try:
|
||||||
|
assignment = await self._get_asset_machine(asset)
|
||||||
|
except ObjectNotFound:
|
||||||
|
raise AssetItemNotAssigned(asset.id)
|
||||||
|
|
||||||
|
if assignment.machine_id is None:
|
||||||
|
raise AssetItemNotAssigned(asset.id)
|
||||||
|
if machine.id != assignment.machine_id:
|
||||||
|
raise AssetItemAssignedToDifferentMachine(asset.id)
|
||||||
|
```
|
||||||
|
(MR !5)
|
||||||
|
|
||||||
|
- **DO** handle external service unavailability explicitly. Whenever calling an external service
|
||||||
|
(e.g. fiscal service), add error handling for the unreachable case. (MR !21)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- **DO** put manager-level behaviour tests in `test_<domain>_manager.py`, not
|
||||||
|
`test_<domain>_endpoints.py`. Endpoint tests should only exercise code that lives in the
|
||||||
|
endpoint itself. Manager tests are faster and easier to isolate. (MR !21)
|
||||||
|
|
||||||
|
- **DO** cover each status transition (e.g. `REPARATION → IN_USE`, `operation_status` changes)
|
||||||
|
with a dedicated test. (MRs !5, !19)
|
||||||
|
|
||||||
|
- **DO** ensure the test DB name in default settings matches the actual local database name used
|
||||||
|
after a fresh clone. (MR !25)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture & Domain Rules
|
||||||
|
|
||||||
|
- **DO** resolve circular imports using `TYPE_CHECKING` guards and
|
||||||
|
`from __future__ import annotations`, not by loosening types to `Any`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from televend_core.databases.televend_repositories.asset_item.model import AssetItem
|
||||||
|
from televend_core.databases.televend_repositories.device import Device
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `@dataclass` (stdlib) rather than Pydantic for internal DTOs when forward references
|
||||||
|
cause Pydantic config errors. (MR !3)
|
||||||
|
|
||||||
|
- **DO** silently create dependent records (e.g. `AssetItemFiscalItalyDevice`) when missing
|
||||||
|
during an update that requires them. Document this in the method docstring. (MR !22)
|
||||||
|
|
||||||
|
- **DO** validate that `is_fiscal=True` is never set on device types (e.g. banknote acceptors)
|
||||||
|
that cannot be fiscal. Enforce at the manager layer. (MRs !14, !15)
|
||||||
|
|
||||||
|
- **DO** look up users by email (unique cross-tenant identifier) rather than by `cloud_user_id`
|
||||||
|
in token-based auth flows. (MR !9)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Git & Process
|
||||||
|
|
||||||
|
- **DO** include the Jira ticket ID in both the branch name and every commit message:
|
||||||
|
`feature/CLOUD-XXXXX-description`, commit `CLOUD-XXXXX: description`. (all MRs)
|
||||||
|
|
||||||
|
- **DO** add `/.venv` to `.gitignore`. (MR !25)
|
||||||
|
|
||||||
|
- **DO** remove unused or transitively-provided dependencies from `pyproject.toml`. (MR !25)
|
||||||
|
|
||||||
|
- **DO** bump the service version in `pyproject.toml` as part of any dependency-upgrade MR.
|
||||||
|
(MR !25)
|
||||||
|
|
||||||
|
- **DO** update `README.md` quickstart section when performing a major runtime upgrade
|
||||||
|
(e.g. Python 3.12 migration). (MR !25)
|
||||||
|
|
||||||
|
- **AVOID** `event_loop` fixture overrides in `conftest.py` — no longer required with modern
|
||||||
|
`pytest-asyncio`. Remove during any Python 3.12 upgrade. (MR !25)
|
||||||
143
REVIEW_GUIDELINES_cashtrack.md
Normal file
143
REVIEW_GUIDELINES_cashtrack.md
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
# Cashtrack Review Guidelines
|
||||||
|
|
||||||
|
Guidelines derived from 61 merged MRs of real code review history. Newer reviews take precedence.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Style & Naming
|
||||||
|
|
||||||
|
- **DO** include the token type in field names when multiple token types exist or may be added.
|
||||||
|
When a field holds only vend tokens, name it accordingly so adding `value_token_diff` later
|
||||||
|
is non-breaking and unambiguous.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# AVOID
|
||||||
|
token_diff: Decimal
|
||||||
|
|
||||||
|
# DO instead
|
||||||
|
vend_token_diff: Decimal
|
||||||
|
```
|
||||||
|
(MR !59)
|
||||||
|
|
||||||
|
- **AVOID** generic `_update_field` helper methods that accept `Any` as a value type. A signature
|
||||||
|
like `_update_field(name: str, value: Any)` eliminates type-checking. Split into individual
|
||||||
|
named update methods with proper type signatures:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# AVOID
|
||||||
|
def _update_field(self, name: str, value: Any) -> None:
|
||||||
|
setattr(self._conform, name, value)
|
||||||
|
|
||||||
|
# DO instead
|
||||||
|
def update_status(self, status: ConformStatus) -> None: ...
|
||||||
|
def update_note(self, note: str) -> None: ...
|
||||||
|
```
|
||||||
|
(MR !40)
|
||||||
|
|
||||||
|
- **DO** keep constants (e.g. filter lists) in a dedicated `const.py` rather than inline in
|
||||||
|
query or view code. (MR !31)
|
||||||
|
|
||||||
|
- **DO** add schema descriptions for non-obvious query parameters (e.g. `collected_after`).
|
||||||
|
(MR !31)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Design
|
||||||
|
|
||||||
|
- **DO** use generics throughout the filter pipeline:
|
||||||
|
```python
|
||||||
|
filter_class: type[FilterBase[T]]
|
||||||
|
manager_method: FilterManagerMethod[T]
|
||||||
|
response_class: type[T]
|
||||||
|
```
|
||||||
|
(MR !16)
|
||||||
|
|
||||||
|
- **DO** default time-bounded endpoints to a sensible range when date params are omitted.
|
||||||
|
`period_stats` defaults to the last 3 months ending today. Document this in the endpoint
|
||||||
|
description. (MR !60)
|
||||||
|
|
||||||
|
- **DO** use request body (not query params) for mutation inputs such as a barcode. (MR !2)
|
||||||
|
|
||||||
|
- **DO** enforce input length limits at the API layer to match DB column constraints:
|
||||||
|
- `note` on collections and conforms: max 500 chars (MR !41)
|
||||||
|
- `barcode` on cashbag conforms: max 128 chars (MR !42)
|
||||||
|
|
||||||
|
- **DO** restrict collection list endpoints to a default 90-day window. (MR !31)
|
||||||
|
|
||||||
|
- **DO** apply the `cash_room=True` flag when querying conforms inside a cash-room context.
|
||||||
|
(MR !40)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling & Exceptions
|
||||||
|
|
||||||
|
- **DO** validate denomination payloads before persisting:
|
||||||
|
- Reject if all denominations and token values are zero. (MR !45)
|
||||||
|
- Reject if the same denomination appears more than once. (MR !51)
|
||||||
|
- Validate quantities against numeric DB constraints before hitting the DB. (MR !50)
|
||||||
|
|
||||||
|
- **DO** enforce that exactly one of two mutually exclusive FKs is set (e.g. either `conform_id`
|
||||||
|
or `collection_id`, never both, never neither). Consider a DB-level `CHECK` constraint. (MR !52)
|
||||||
|
|
||||||
|
- **DO** skip unknown `changed_field` values in the changelog endpoint rather than raising.
|
||||||
|
(MR !54)
|
||||||
|
|
||||||
|
- **DO** treat `null` collection status as `open`. (MR !49)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- **DO** write tests in the same MR as the feature, not in a follow-up.
|
||||||
|
|
||||||
|
- **DO** cover filter behaviour at the endpoint level, including interactions with data-access
|
||||||
|
restrictions. (MRs !55, !48)
|
||||||
|
|
||||||
|
- **DO** include a changelog entry in every MR that adds or changes user-facing behaviour.
|
||||||
|
(MR !59)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture & Domain Rules
|
||||||
|
|
||||||
|
- **DO** split query functions so each returns exactly one result shape. A function that computes
|
||||||
|
two unrelated shapes should be two functions. (MR !59)
|
||||||
|
|
||||||
|
- **DO** always filter conform queries with `CashBagConform.alive = True`. This is the soft-delete
|
||||||
|
sentinel; omitting it causes deleted conforms to leak into results. Verify the `alive` condition
|
||||||
|
is present whenever you add a new join through the conform table. (MR !57)
|
||||||
|
|
||||||
|
- **DO** use soft-delete (`alive` flag) for denominations. Cascade delete was removed (MR !39)
|
||||||
|
after soft-delete was introduced (MR !36) to preserve historical counting records.
|
||||||
|
|
||||||
|
- **DO** reuse an existing conform on import rather than always inserting a new one. Look up by
|
||||||
|
natural key (barcode + cashroom) before creating. (MR !63)
|
||||||
|
|
||||||
|
- **DO** recalculate `total_coins` / `total_bills` from denominations whenever denominations are
|
||||||
|
updated — never accept totals directly from the caller. (MR !31)
|
||||||
|
|
||||||
|
- **DO** place manager classes in a `managers/` sub-folder, separate from endpoints and queries.
|
||||||
|
(MR !7)
|
||||||
|
|
||||||
|
- **DO** attach `cashroom_id` to cashbag conform records for proper cash-room scoping. (MR !34)
|
||||||
|
|
||||||
|
- **DO** wire data-access (company + user) restrictions into every new list or filter endpoint
|
||||||
|
before the MR is ready for review. (MRs !48, !55)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Git & Process
|
||||||
|
|
||||||
|
- **DO** use branch names `feature/CLOUD-XXXXX-short-description` for all work targeting
|
||||||
|
`develop`.
|
||||||
|
|
||||||
|
- **DO** title MRs as `CLOUD-XXXXX: description` (imperative). Avoid the "Resolve" prefix.
|
||||||
|
(MR !64)
|
||||||
|
|
||||||
|
- **DO** keep MRs focused on a single ticket. Preliminary or follow-up changes belong in their
|
||||||
|
own MR. (MRs !59, !60 pattern)
|
||||||
|
|
||||||
|
- **DO** resolve all open review threads before merging. (MRs !59, !16)
|
||||||
|
|
||||||
|
- **DO** ensure ruff passes before requesting review. A cleanup-only "Ruff is happy" MR is a
|
||||||
|
sign that formatting discipline slipped. (MRs !61, !62)
|
||||||
439
REVIEW_GUIDELINES_general.md
Normal file
439
REVIEW_GUIDELINES_general.md
Normal file
@ -0,0 +1,439 @@
|
|||||||
|
# General Review Guidelines
|
||||||
|
|
||||||
|
Derived from real code review history across three services:
|
||||||
|
- **planogrammer** — 128 MRs, 954 comments
|
||||||
|
- **cashtrack** — 61 MRs, 238 comments
|
||||||
|
- **assetor** — 28 MRs, 195 comments
|
||||||
|
|
||||||
|
Rules confirmed across multiple services are marked with the services that evidence them.
|
||||||
|
Newer reviews take precedence over older ones within each service.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Style & Naming
|
||||||
|
|
||||||
|
- **DO** name variables to reflect their precise meaning. The name should answer "what is this
|
||||||
|
a collection of?" Prefer `non_existing_ids` over `missing`, `changed_column_ids` over
|
||||||
|
`changes`. *(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** name boolean variables so they read as a statement:
|
||||||
|
```python
|
||||||
|
is_changed = column.id in changed_column_ids
|
||||||
|
```
|
||||||
|
*(planogrammer)*
|
||||||
|
|
||||||
|
- **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
|
||||||
|
```
|
||||||
|
*(planogrammer)*
|
||||||
|
|
||||||
|
- **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 ...
|
||||||
|
```
|
||||||
|
*(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** use the `X | None` syntax for optional type hints:
|
||||||
|
```python
|
||||||
|
column_filter: PushPlanogramColumnFilterDto | None = None
|
||||||
|
```
|
||||||
|
*(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** use `Iterable[T]` for input parameters that only need to be iterated, not `List[T]`.
|
||||||
|
Accept the most general type; return the most specific type. *(assetor)*
|
||||||
|
|
||||||
|
- **DO** use typed dataclasses for internal structured return values and error payloads rather
|
||||||
|
than plain dicts or multi-value tuples:
|
||||||
|
```python
|
||||||
|
# AVOID
|
||||||
|
(asset_per_device_id, company_brand_ids, failed_responses) = await manager.bulk_create_preload(...)
|
||||||
|
failed_responses.append({"device_id": device.id, "reason": "..."})
|
||||||
|
|
||||||
|
# DO instead
|
||||||
|
@dataclass
|
||||||
|
class BulkPreloadResult:
|
||||||
|
asset_per_device_id: dict
|
||||||
|
failed_responses: list[FailedResponse]
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FailedResponse:
|
||||||
|
device_id: int
|
||||||
|
reason: str
|
||||||
|
```
|
||||||
|
Consider static factory methods for repeated reason strings:
|
||||||
|
`FailedResponse.for_device_already_has_asset(device_id)`. *(assetor)*
|
||||||
|
|
||||||
|
- **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. *(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** suffix all custom exception classes with `Exception`, e.g. `UnableToDeleteColumnException`,
|
||||||
|
`BulkLimitExceededException`. *(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** use single `_` prefix for protected helpers, double `__` prefix for private
|
||||||
|
module-level functions (name mangling). The convention is: `_` = protected, `__` = private.
|
||||||
|
*(planogrammer)*
|
||||||
|
|
||||||
|
- **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
|
||||||
|
```
|
||||||
|
*(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** extract repeated small operations into named helper functions. This removes duplication
|
||||||
|
and makes the calling code read at a consistent level of abstraction:
|
||||||
|
```python
|
||||||
|
def _clear_changed_prices(column): ...
|
||||||
|
def _get_price_or_none(price): ...
|
||||||
|
```
|
||||||
|
*(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** invert conditionals to enable early-return/continue patterns:
|
||||||
|
```python
|
||||||
|
if not is_changed:
|
||||||
|
_clear_changed_prices(column)
|
||||||
|
continue
|
||||||
|
# handle the positive case at normal indentation
|
||||||
|
```
|
||||||
|
*(planogrammer)*
|
||||||
|
|
||||||
|
- **AVOID** generic update helpers that accept `Any`. A signature like
|
||||||
|
`_update_field(name: str, value: Any)` eliminates type-checking. Use individual named methods:
|
||||||
|
```python
|
||||||
|
# AVOID
|
||||||
|
def _update_field(self, name: str, value: Any) -> None:
|
||||||
|
setattr(self._conform, name, value)
|
||||||
|
|
||||||
|
# DO instead
|
||||||
|
def update_status(self, status: ConformStatus) -> None: ...
|
||||||
|
def update_note(self, note: str) -> None: ...
|
||||||
|
```
|
||||||
|
*(cashtrack)*
|
||||||
|
|
||||||
|
- **DO** include the type in field names when multiple variants exist or may be added:
|
||||||
|
```python
|
||||||
|
# AVOID — breaks when value_token_diff is added later
|
||||||
|
token_diff: Decimal
|
||||||
|
|
||||||
|
# DO instead
|
||||||
|
vend_token_diff: Decimal
|
||||||
|
```
|
||||||
|
*(cashtrack)*
|
||||||
|
|
||||||
|
- **DO** keep constants (filter lists, limits, defaults) in a dedicated `const.py` rather than
|
||||||
|
inline in query or view code. *(cashtrack)*
|
||||||
|
|
||||||
|
- **DO** add schema descriptions for non-obvious query parameters. *(cashtrack)*
|
||||||
|
|
||||||
|
- **DO** order method parameters hierarchically: `tenant_id` before `company_id`, more general
|
||||||
|
before more specific. Apply consistently across all call sites. *(assetor)*
|
||||||
|
|
||||||
|
- **AVOID** `datetime.utcnow()` in new code. Use `datetime.now(UTC)` — `utcnow()` is deprecated
|
||||||
|
in Python 3.12+. *(assetor, planogrammer: use project utility module)*
|
||||||
|
|
||||||
|
- **AVOID** using `assert` in production/runtime code. Assertions are stripped with `-O`.
|
||||||
|
Raise an explicit exception instead. *(planogrammer)*
|
||||||
|
|
||||||
|
- **AVOID** tabs; use spaces. Run `ruff format` before pushing. Install pre-commit hooks so
|
||||||
|
formatting is enforced automatically on every commit. *(planogrammer, cashtrack, assetor)*
|
||||||
|
|
||||||
|
- **AVOID** broad ruff/mypy ignore rules in `pyproject.toml`. Verify each suppression is
|
||||||
|
genuinely needed by running `pre-commit run --all-files`. *(assetor)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Design
|
||||||
|
|
||||||
|
- **DO** place each new domain in its own folder with its own `endpoints.py`, `manager.py`,
|
||||||
|
`dto.py`, and `exceptions.py`. Don't embed new domain read logic in an existing domain's
|
||||||
|
folder. *(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** add an upper-bound validation on all bulk list inputs:
|
||||||
|
```python
|
||||||
|
MAX_GET_PRODUCT_BULK_IDS = 1000
|
||||||
|
if len(ids) > MAX_GET_PRODUCT_BULK_IDS:
|
||||||
|
raise BulkLimitExceededException(...)
|
||||||
|
```
|
||||||
|
*(planogrammer, cashtrack)*
|
||||||
|
|
||||||
|
- **DO** enforce input length limits at the API layer to match DB column constraints:
|
||||||
|
- `note` fields: max 500 chars
|
||||||
|
- `barcode` fields: max 128 chars
|
||||||
|
*(cashtrack)*
|
||||||
|
|
||||||
|
- **DO** use request body (not query params) for mutation inputs. *(cashtrack)*
|
||||||
|
|
||||||
|
- **DO** default time-bounded list endpoints to a sensible window when date params are omitted
|
||||||
|
(e.g. last 90 days). Document the default in the endpoint description. *(cashtrack)*
|
||||||
|
|
||||||
|
- **DO** make parameters mandatory when they are always required for business logic. Optional
|
||||||
|
parameters that are always supplied create false API flexibility. *(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** use mutually exclusive validation 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
|
||||||
|
```
|
||||||
|
*(planogrammer, cashtrack)*
|
||||||
|
|
||||||
|
- **DO** use the correct HTTP status codes:
|
||||||
|
- `404` when a referenced resource is not found (not `400` or `500`) *(assetor)*
|
||||||
|
- `409 Conflict` for business-rule violations unrelated to authorization (not `403`) *(planogrammer)*
|
||||||
|
|
||||||
|
- **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 transactions
|
||||||
|
roll back correctly on exception. *(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** use `company_exists(company_id)` when you only need to verify existence; reserve
|
||||||
|
`get_company` for when you need the object's fields. *(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** write query parameter help text in imperative form. Don't expose internal
|
||||||
|
implementation details (e.g. which database is queried) in OpenAPI docs. *(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** keep success responses for void operations minimal — a short `"ok"` or empty 204,
|
||||||
|
not a verbose success DTO. *(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** use SQLAlchemy 2.x style in all new queries:
|
||||||
|
- `select(Model)` not `select([Model])`
|
||||||
|
- `(condition, value)` not `[(condition, value)]` in CASE expressions
|
||||||
|
- Add `.mappings()` to raw SQL results for dictionary-like access
|
||||||
|
*(assetor)*
|
||||||
|
|
||||||
|
- **AVOID** kebab-case query parameter names when the rest of the service uses snake_case.
|
||||||
|
*(planogrammer)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling & Exceptions
|
||||||
|
|
||||||
|
- **DO** place domain-specific exceptions in that domain's own `exceptions.py`. If an exception
|
||||||
|
is only raised within one flow, it belongs in that flow's module. *(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** add a catch-all handler in every endpoint for the base exception class of your domain
|
||||||
|
exceptions. This ensures unhandled domain errors return a structured response rather than 500.
|
||||||
|
*(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** narrow `try/except` blocks to only the lines that can raise the caught exception:
|
||||||
|
```python
|
||||||
|
# AVOID — business logic inside the try block catches unintended exceptions
|
||||||
|
try:
|
||||||
|
assignment = await self._get_asset_machine(asset)
|
||||||
|
if assignment.machine_id is None:
|
||||||
|
raise AssetItemNotAssigned(asset.id)
|
||||||
|
except ObjectNotFound:
|
||||||
|
raise AssetItemNotAssigned(asset.id)
|
||||||
|
|
||||||
|
# DO instead
|
||||||
|
try:
|
||||||
|
assignment = await self._get_asset_machine(asset)
|
||||||
|
except ObjectNotFound:
|
||||||
|
raise AssetItemNotAssigned(asset.id)
|
||||||
|
|
||||||
|
if assignment.machine_id is None:
|
||||||
|
raise AssetItemNotAssigned(asset.id)
|
||||||
|
```
|
||||||
|
*(assetor)*
|
||||||
|
|
||||||
|
- **DO** log the caught exception (at least at `DEBUG` level) when swallowing or transforming
|
||||||
|
it, so the original cause remains traceable in logs. *(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** validate payloads thoroughly before persisting:
|
||||||
|
- Reject if all denomination/token values are zero
|
||||||
|
- Reject duplicate entries in the same payload
|
||||||
|
- Validate numeric values against DB column constraints before hitting the DB
|
||||||
|
*(cashtrack)*
|
||||||
|
|
||||||
|
- **DO** handle external service unavailability explicitly. Always add error handling for the
|
||||||
|
unreachable case when calling external services. *(assetor)*
|
||||||
|
|
||||||
|
- **DO** skip unknown enum/field values in changelog and audit endpoints rather than raising.
|
||||||
|
*(cashtrack)*
|
||||||
|
|
||||||
|
- **AVOID** raising a generic `Exception` for domain-specific error conditions. Define a named
|
||||||
|
exception class. *(planogrammer)*
|
||||||
|
|
||||||
|
- **AVOID** importing the entire `exceptions` module — import only the specific class you need.
|
||||||
|
*(planogrammer)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- **DO** write tests in the same MR as the feature. *(cashtrack, planogrammer)*
|
||||||
|
|
||||||
|
- **DO** assert on IDs only when verifying list/collection responses, not on 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. *(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** put manager-level behaviour tests in `test_<domain>_manager.py`, not in endpoint
|
||||||
|
tests. Endpoint tests should only exercise endpoint-layer code. *(assetor)*
|
||||||
|
|
||||||
|
- **DO** cover every status transition with a dedicated test. *(assetor, cashtrack)*
|
||||||
|
|
||||||
|
- **DO** cover filter behaviour at the endpoint level, including interactions with data-access
|
||||||
|
restrictions. *(cashtrack)*
|
||||||
|
|
||||||
|
- **DO** add tests for prices with 4 decimal places — this edge case has caused production bugs.
|
||||||
|
*(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** write a serialization test for every response DTO that contains `Decimal` or `datetime`
|
||||||
|
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)
|
||||||
|
```
|
||||||
|
*(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** use standard `setup_method` / `teardown_method` for synchronous test setup/teardown.
|
||||||
|
Only use `async` fixtures when you genuinely need to `await` inside them. *(planogrammer)*
|
||||||
|
|
||||||
|
- **AVOID** hard-coding specific auto-increment IDs in test assertions. Use the IDs from the
|
||||||
|
objects created in test setup. *(planogrammer)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture & Domain Rules
|
||||||
|
|
||||||
|
- **DO** fetch company/entity once per request and pass the object downstream. Never call
|
||||||
|
`get_company` (or equivalent) more than once in the same request path. *(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** move entity existence checks into manager/service methods, not the endpoint layer.
|
||||||
|
*(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** batch multiple individual DB queries into a single `list` query where possible.
|
||||||
|
*(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** decompose large methods that do multiple conceptually distinct things. One method should
|
||||||
|
do one thing. *(planogrammer, cashtrack)*
|
||||||
|
|
||||||
|
- **DO** keep manager responsibilities focused on a single flow. Read/GET logic and write logic
|
||||||
|
belong in separate managers. *(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** initialize collaborating managers in the constructor, not lazily inside methods.
|
||||||
|
*(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** use `save_many(entities)` for bulk saves instead of looping `save(entity)`. *(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** pass already-loaded ORM objects to methods rather than re-fetching by ID. *(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** prefer explicit `save()` calls over relying on implicit ORM dirty-tracking. Explicit
|
||||||
|
saves make the persistence boundary visible. *(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** prefer a direct field-update query over a fetch-then-modify-then-save pattern when
|
||||||
|
updating a single field. *(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** use soft-delete (`alive` flag or equivalent) to preserve historical records. Always
|
||||||
|
filter queries with the soft-delete sentinel — omitting it causes deleted records to leak into
|
||||||
|
results. *(cashtrack)*
|
||||||
|
|
||||||
|
- **DO** look up by natural key before creating on import operations. Reuse an existing record
|
||||||
|
if it already exists rather than always inserting. *(cashtrack)*
|
||||||
|
|
||||||
|
- **DO** recalculate derived totals (e.g. `total_coins`, `total_bills`) from source data
|
||||||
|
whenever source data changes — never accept pre-calculated totals from the caller. *(cashtrack)*
|
||||||
|
|
||||||
|
- **DO** wire data-access (company + user) restrictions into every new list or filter endpoint
|
||||||
|
before the MR is ready for review. *(cashtrack)*
|
||||||
|
|
||||||
|
- **DO** resolve circular imports using `TYPE_CHECKING` guards and `from __future__ import annotations`:
|
||||||
|
```python
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from televend_core.databases.televend_repositories.asset_item.model import AssetItem
|
||||||
|
```
|
||||||
|
Use `@dataclass` (stdlib) rather than Pydantic for internal DTOs when forward references
|
||||||
|
cause Pydantic config errors. *(assetor)*
|
||||||
|
|
||||||
|
- **AVOID** nesting context managers from different sources. Use one transaction boundary per
|
||||||
|
operation. *(planogrammer)*
|
||||||
|
|
||||||
|
- **AVOID** the deprecated `load_options` parameter on repository calls. Use typed `LoadOptions`
|
||||||
|
classes from `televend-core`. *(planogrammer)*
|
||||||
|
|
||||||
|
- **AVOID** generalizing too early. If two similar pieces of code would diverge with the next
|
||||||
|
requirement, keep them separate (Rule of Three). *(planogrammer)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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, ...)
|
||||||
|
```
|
||||||
|
*(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** use `model_dump_json()` (not `model_dump()`) when serializing DTOs that contain
|
||||||
|
`Decimal` or `datetime` fields, to ensure correct JSON types. *(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** use `ConfigDict` consistently in all Pydantic v2 models. Don't mix old `class Config`
|
||||||
|
style with `model_config = ConfigDict(...)` in the same codebase. *(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** give optional Pydantic fields an explicit `default=None`:
|
||||||
|
```python
|
||||||
|
class MyDto(BaseModel):
|
||||||
|
filter: str | None = None
|
||||||
|
```
|
||||||
|
*(planogrammer)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Git & Process
|
||||||
|
|
||||||
|
- **DO** name feature branches `feature/CLOUD-NNNNN-short-description`. The `feature/` prefix
|
||||||
|
is required for CI/CD pipeline triggers. *(planogrammer, cashtrack, assetor)*
|
||||||
|
|
||||||
|
- **DO** title MRs as `CLOUD-NNNNN: description` (imperative, present tense). Avoid the
|
||||||
|
"Resolve" prefix auto-generated by GitLab. *(cashtrack)*
|
||||||
|
|
||||||
|
- **DO** include the full endpoint path in CHANGELOG entries:
|
||||||
|
- Bad: `Added new endpoint`
|
||||||
|
- Good: `POST /v1/tenants/{tenant_id}/companies/{company_id}/planogram-change-requests/push/bulk`
|
||||||
|
*(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** keep MRs focused on a single ticket. Preliminary or follow-up changes belong in
|
||||||
|
their own MR. *(cashtrack)*
|
||||||
|
|
||||||
|
- **DO** rebase your feature branch on `develop` before requesting review. *(planogrammer)*
|
||||||
|
|
||||||
|
- **DO** resolve all open review threads before merging. *(cashtrack, planogrammer)*
|
||||||
|
|
||||||
|
- **DO** ensure ruff passes before requesting review — a cleanup-only "Ruff is happy" MR is
|
||||||
|
a sign that formatting discipline slipped. *(cashtrack, assetor, planogrammer)*
|
||||||
|
|
||||||
|
- **DO** add `/.venv` to `.gitignore`. *(assetor)*
|
||||||
|
|
||||||
|
- **DO** bump the service version in `pyproject.toml` as part of any dependency-upgrade MR.
|
||||||
|
*(assetor)*
|
||||||
|
|
||||||
|
- **DO** update `README.md` when performing a major runtime upgrade. *(assetor)*
|
||||||
|
|
||||||
|
- **AVOID** `event_loop` fixture overrides in `conftest.py` — no longer required with modern
|
||||||
|
`pytest-asyncio`. *(assetor)*
|
||||||
|
|
||||||
|
- **AVOID** `[ci-skip]` commits for changelog-only changes unless the change truly has no code
|
||||||
|
impact. *(planogrammer)*
|
||||||
336
REVIEW_GUIDELINES_planogrammer.md
Normal file
336
REVIEW_GUIDELINES_planogrammer.md
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
# 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)
|
||||||
Reference in New Issue
Block a user