---
name: tap-ows-service
description: Coding conventions for the TAP team's ows-payment and ows-payee Python/Flask microservices. Use when adding, modifying, or reviewing endpoints, blueprints, logic, repositories, models, schemas, or tests in those repos. Triggers on phrases like "add an endpoint to ows-payment", "create a new resource in ows-payee", "review my blueprint", "wire up a new schema", "fix the soft-delete on this model", "write tests for this handler". SKIP if the repo is not ows-payment or ows-payee (e.g. lambda-*, ows-* outside TAP, non-Python services).
---

# Coding Conventions for ows-payment and ows-payee

**Tech Stack:**
- Python
- Flask (API)
- SQLAlchemy (ORM)
- Marshmallow (Serialization/Validation)
- pytest (Testing)
- Swagger schema auto-generated via Flask endpoints and Marshmallow schemas using custom `flaskapispec` tool.

**Where these symbols live.** Names referenced below come from different places — verify before importing:

- `@check_access`, `LogicError` — defined per-repo. Grep the target repo for the exact module.
- `BaseSoftDeleteModel`, `BaseModel` — the `abacus_common_logic` shared library.
- `ma`, `ma.Nested`, custom marshalling fields — `abacus_common_logic.marshalling.custom_fields`.


## When to load references

Do not preload. Load each reference exactly when the condition is met:

- About to write or review functional tests → [references/functional-tests.md](./references/functional-tests.md)
- About to write or review unit tests → [references/unit-tests.md](./references/unit-tests.md)

---

## Architecture Layers

1. **Blueprints**: `project/blueprints/<resource>.py`
   - Flask Blueprint endpoints
   - Declarative style with decorators: `@route`, `@doc`, `@check_access`, `@use_kwargs`, `@marshal_with`
2. **Logic**: `project/logic/<resource>.py`
   - Business orchestration
   - Returns models/dicts/dataclasses
   - No HTTP layer imports/logic
3. **Repository**: `project/repository/<resource>.py`
   - DB querying helpers for complex logic
   - Returns `(items, total_count)`, dataclasses, or dicts
   - No HTTP layer imports/logic
4. **Models**: `project/models/<resource>.py`
   - SQLAlchemy model (soft-delete aware)
   - No HTTP layer imports/logic, no business logic
5. **Schemas**: `project/schemas/<resource>.py`
   - Marshmallow schemas for request validation and response serialization
   - See [Schemas](#schemas) for the required set per resource

> **Tip:** Favor small functions; keep each layer focused.

---

## Endpoint Style

- Use `HTTPStatus` from `http`.
- **Decorator order:**
  1. `@<blueprint>.route(..., methods=[...])`
  2. `@doc(...)`
  3. `@check_access`
  4. `@use_kwargs(...)` (pagination query first, then JSON schema)
  5. `@marshal_with(...)` (success schema + code)
- Return `(result, HTTPStatus.<CODE>)`.
- **DELETE:**
  - `@marshal_with(None, code=HTTPStatus.NO_CONTENT, apply=False)`
  - Return `None, HTTPStatus.NO_CONTENT`

---

## Routing Conventions

| Action | Route                                 | Example                            |
|--------|---------------------------------------|------------------------------------|
| Create | `POST /<resource>`                    | `POST /payment`                    |
| Get    | `GET /<resource>/<int:{id_field}>`    | `GET /payment/<int:payment_id>`    |
| Delete | `DELETE /<resource>/<int:{id_field}>` | `DELETE /payment/<int:payment_id>` |
| Bulk   | `POST /<resource>/bulk`               | `POST /payment/bulk`               |

- **Bulk List:**
  - Pagination via query params (`PaginationSchema`)
  - Filters as JSON: `{ "filters": {...} }` via `<Resource>FilterParamsPostSchema`
  - In handler: `filters = payload.pop("filters", {})`, extract filter values, call logic

---

## Logic Style

- No HTTP layer imports (e.g., no `abort`).
- Return models/dicts/dataclasses, not HTTP responses.
- Raise custom exceptions for business rule violations (`LogicError` or subclasses; defined locally in each repo).
- Keep only high-level business logic/orchestration; complex querying goes in the models or repository layer.

---

## Schemas

- Define in `project/schemas/<resource>.py` using `ma` from `abacus_common_logic.marshalling.custom_fields`.
- Provide:
  - `<Resource>PostSchema`
  - `<Resource>DetailSchema`
  - `<Resource>FilterKeySchema`
  - `<Resource>FilterParamsPostSchema` (with `filters = ma.Nested(..., load_default={})`)
  - `<Resource>ListSchema` (with `items` + `total_count`)
- Use `@validates_schema` for cross-field validation; raise `ValidationError(field_name=...)`.

---

## Model & Soft Delete

- Model subclasses `BaseSoftDeleteModel` (or `BaseModel` when soft-delete is not needed) — both are defined in the `abacus_common_logic` shared library.
- Implement:
  - `get_by_id()` (excludes soft-deleted: `deleted_at is None`)
  - `get_by_id_or_error()` (calls `abort(code=404, description="<Model> with id X not found")`)
  - `delete_by_id_or_error()`:
    - Loads via `get_by_id_or_error`
    - Enforces business rules (abort 400 on violation)
    - Calls `_soft_delete()` and commits (`db.session.commit()`)

---

## Repository Querying

- List function returns `(items, total_count)` or `models.generic.Items` dataclass.
- Always filter out deleted: `Model.query.filter(Model.deleted_at == None)`.
- Compute `total_count = query.count()` **before** pagination.
- Default order: `Model.default_order()` (usually `created_at.desc()`).
- Apply `offset` only if truthy; apply `limit` only if truthy.

---

## Error Semantics

- Primary resource (the URL-addressed one) missing or soft-deleted → 404
- Related entity required by business logic missing (e.g., FK target) → 400
- Other business rule violation → 400

---

## Testing

Two test types, each with its own reference (load on demand — see [When to load references](#when-to-load-references)):

- **Functional tests** — high-level, hit real endpoints, patch only external integrations. See [references/functional-tests.md](./references/functional-tests.md).
- **Unit tests** — single layer, patch dependencies, use `call_args_list` for mock assertions. See [references/unit-tests.md](./references/unit-tests.md).

Per-resource minimum coverage (functional):

- GET on a soft-deleted record returns 404
- DELETE success returns 204 with empty body
- Second DELETE on the same id returns 404

---

## New Endpoint / Resource Checklist

Touch each layer in this order. File paths are conventions; grep an existing resource (e.g. `report_payment`) for a working template.

- [ ] **Model** — `project/models/<resource>.py`. Subclass `BaseSoftDeleteModel` (or `BaseModel` when soft-delete is not needed) from `abacus_common_logic`. Implement `get_by_id`, `get_by_id_or_error`, `delete_by_id_or_error`.
- [ ] **Repository** — `project/repository/<resource>.py` if querying is non-trivial. Filter `deleted_at == None`, compute `total_count` before pagination.
- [ ] **Logic** — `project/logic/<resource>.py`. Orchestration only; raise `LogicError` subclasses for business rule violations; no HTTP imports.
- [ ] **Schemas** — `project/schemas/<resource>.py`. Add `<Resource>PostSchema`, `<Resource>DetailSchema`, `<Resource>FilterKeySchema`, `<Resource>FilterParamsPostSchema`, `<Resource>ListSchema`. Add cross-field validation with `@validates_schema` where applicable.
- [ ] **Blueprint** — `project/blueprints/<resource>.py`. Follow the decorator order in [Endpoint Style](#endpoint-style). Register the blueprint with the app.
- [ ] **Unit tests** — handler test (mock logic, spy schemas) and logic tests (mock model/repository).
- [ ] **Functional tests** — cover the three minimum cases above plus happy-path create/get/list.
