diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index dbee977..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.gitignore b/.gitignore index 7a60b85..b908d4c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ __pycache__/ *.pyc +.DS_Store diff --git a/dlv_fleetflow_contactflow/CHANGELOG.md b/dlv_fleetflow_contactflow/CHANGELOG.md new file mode 100644 index 0000000..7ebde54 --- /dev/null +++ b/dlv_fleetflow_contactflow/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2026-06-14 + +### Added +- Initial release. +- `GET /fleetflow/contactflow/drivers` endpoint returning active trip drivers as JSON contacts. +- Bearer API key authentication via Odoo native API keys (`res.users.apikeys`, `rpc` scope). +- Optional `location` query parameter to filter drivers on `location_id.name`. diff --git a/dlv_fleetflow_contactflow/README.md b/dlv_fleetflow_contactflow/README.md new file mode 100644 index 0000000..be12488 --- /dev/null +++ b/dlv_fleetflow_contactflow/README.md @@ -0,0 +1,126 @@ +# dlv_fleetflow_contactflow + +## Overview + +FleetFlow ContactFlow API exposes the active trip drivers managed by the +`busenco_custom` addon (`trip.driver`) over a token-authenticated HTTP endpoint +that returns JSON contacts. It is designed to feed external systems — such as a +WordPress ContactFlow integration — with an always-current driver contact list, +optionally filtered by office location. + +Driver names are stored as a single `name` field on `trip.driver`. The endpoint +splits `trip.driver.name` on the **first space** into `first_name` and +`last_name`: + +- `"Jan Peeters"` → `first_name="Jan"`, `last_name="Peeters"` +- `"Jan Van den Berg"` → `first_name="Jan"`, `last_name="Van den Berg"` +- `"Jan"` (no space) → `first_name="Jan"`, `last_name=""` + +## Dependencies + +- `base` +- `busenco_custom` (provides the `trip.driver` model) + +## Endpoint + +### URL + +``` +GET /fleetflow/contactflow/drivers +``` + +Optional query parameter: + +| Param | Description | +|-------|-------------| +| `location` | Filter drivers on `location_id.name` (exact match) | + +Example: + +``` +GET /fleetflow/contactflow/drivers?location=Antwerpen +``` + +### Authentication + +Bearer token using Odoo's **native API keys**. + +``` +Authorization: Bearer +``` + +The key is validated against `res.users.apikeys` with the `rpc` scope. A missing, +malformed, or invalid key returns **401 Unauthorized**. + +Create a key in Odoo: **Settings → Users → (select user) → Account Security → +API Keys → New API Key**. + +### Response format + +`Content-Type: application/json;charset=utf-8` + +```json +{ + "contacts": [ + { + "id": "42", + "first_name": "Jan", + "last_name": "Peeters", + "phone": "+3231234567", + "mobile": "+32475123456", + "email": "jan.peeters@example.com" + } + ] +} +``` + +On error the endpoint returns **500** with: + +```json +{ "error": "" } +``` + +### Field mapping + +| JSON field | Source (`trip.driver`) | Notes | +|--------------|-------------------------------|-------| +| `id` | `id` | Cast to string | +| `first_name` | `name` (before first space) | Split on first space | +| `last_name` | `name` (after first space) | Remainder after first space; `""` if none | +| `phone` | `phone_number` | `""` if empty | +| `mobile` | `phone_number2` | `""` if empty | +| `email` | `email` | `""` if empty | + +Only drivers with `active = True` are returned, ordered by `name` ascending. + +## WordPress setup + +1. Store the Odoo base URL and the API key in your WordPress configuration + (e.g. as constants in `wp-config.php` or via your integration plugin's + settings), never hard-coded in templates. +2. Make a server-side `GET` request to + `https:///fleetflow/contactflow/drivers` with the header + `Authorization: Bearer `. +3. Add the `location` query parameter if you only want drivers for a specific + office location. +4. Parse the `contacts` array from the JSON response and render it through your + ContactFlow templates. +5. Cache the response (e.g. a transient) to avoid polling Odoo on every page + load. + +## Installation steps + +1. Copy the `dlv_fleetflow_contactflow` directory into your Odoo addons path + (alongside `busenco_custom`). +2. Restart the Odoo server. +3. Enable **Developer Mode** and update the apps list + (**Apps → Update Apps List**). +4. Search for **FleetFlow ContactFlow API** and click **Install**. +5. Create an API key for the user that should own the feed + (**Settings → Users → Account Security → API Keys → New API Key**). +6. Test the endpoint: + + ```bash + curl -H "Authorization: Bearer " \ + "https:///fleetflow/contactflow/drivers" + ``` diff --git a/dlv_fleetflow_contactflow/ROADMAP.md b/dlv_fleetflow_contactflow/ROADMAP.md new file mode 100644 index 0000000..4033a9a --- /dev/null +++ b/dlv_fleetflow_contactflow/ROADMAP.md @@ -0,0 +1,28 @@ +# Roadmap + +Planned extensions to the FleetFlow ContactFlow API addon. Items are grouped by +target release and are subject to change. + +## v1.1 — Additional feed types (buses, locations) + +- Add `GET /fleetflow/contactflow/buses` exposing `trip.bus` records. +- Add `GET /fleetflow/contactflow/locations` exposing `office.trip.location` records. +- Share the authentication and JSON-response plumbing across all feed types. + +## v1.2 — Per-category field selection + +- Support a `fields` query parameter so callers receive only the fields they + request (e.g. `?fields=first_name,last_name,email`). +- Reduce payload size and decouple consumers from the full schema. + +## v2.0 — Rename busenco_custom to fleetflow + +- Migrate the dependency from `busenco_custom` to the renamed `fleetflow` addon. +- Update model references and the manifest `depends` accordingly. +- Provide a migration path for existing installations. + +## Future — Webhook push on driver change + +- Emit a webhook to subscribed endpoints when a driver record changes, instead + of relying on consumers polling the feed. +- Allow registration and management of webhook subscriptions. diff --git a/dlv_fleetflow_contactflow/__init__.py b/dlv_fleetflow_contactflow/__init__.py new file mode 100644 index 0000000..3b38916 --- /dev/null +++ b/dlv_fleetflow_contactflow/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import models +from . import controllers diff --git a/dlv_fleetflow_contactflow/__manifest__.py b/dlv_fleetflow_contactflow/__manifest__.py new file mode 100644 index 0000000..fad816a --- /dev/null +++ b/dlv_fleetflow_contactflow/__manifest__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +{ + "name": "FleetFlow ContactFlow API", + "summary": "Expose Busenco trip drivers as a ContactFlow JSON API " + "secured by an API key.", + "description": """ + FleetFlow ContactFlow API + ========================= + + Provides a token-authenticated HTTP endpoint that returns the active + trip drivers (busenco_custom ``trip.driver``) as JSON contacts, + optionally filtered by office location. + """, + "author": "bv Domus La Vila", + "website": "https://domuslavila.eu", + "category": "Technical", + "version": "16.0.1.0.0", + "license": "LGPL-3", + "depends": ["base", "busenco_custom"], + "data": [], + "installable": True, + "application": False, + "auto_install": False, +} diff --git a/dlv_fleetflow_contactflow/controllers/__init__.py b/dlv_fleetflow_contactflow/controllers/__init__.py new file mode 100644 index 0000000..757b12a --- /dev/null +++ b/dlv_fleetflow_contactflow/controllers/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import main diff --git a/dlv_fleetflow_contactflow/controllers/main.py b/dlv_fleetflow_contactflow/controllers/main.py new file mode 100644 index 0000000..ee7389e --- /dev/null +++ b/dlv_fleetflow_contactflow/controllers/main.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +import json +import logging + +from odoo import http +from odoo.http import request + +_logger = logging.getLogger(__name__) + + +class FleetFlowContactFlowController(http.Controller): + + @http.route( + "/fleetflow/contactflow/drivers", + type="http", + auth="fleetflow_api_key", + methods=["GET"], + csrf=False, + ) + def fleetflow_contactflow_drivers(self, location=None, **kwargs): + """Return active trip drivers as ContactFlow JSON contacts. + + Optional query param ``location`` filters drivers on + ``location_id.name``. + """ + try: + domain = [("active", "=", True)] + if location: + domain.append(("location_id.name", "=", location)) + + drivers = request.env["trip.driver"].sudo().search( + domain, order="name asc" + ) + + contacts = [] + for d in drivers: + name = d.name or "" + first_name, sep, last_name = name.partition(" ") + contacts.append({ + "id": str(d.id), + "first_name": first_name, + "last_name": last_name, + "phone": d.phone_number or "", + "mobile": d.phone_number2 or "", + "email": d.email or "", + }) + + _logger.info( + "FleetFlow ContactFlow: returned %s drivers (location=%s)", + len(contacts), location, + ) + + return request.make_response( + json.dumps({"contacts": contacts}), + headers=[ + ("Content-Type", "application/json;charset=utf-8"), + ], + ) + except Exception as exc: + _logger.exception("FleetFlow ContactFlow: error building response") + return request.make_response( + json.dumps({"error": str(exc)}), + headers=[ + ("Content-Type", "application/json;charset=utf-8"), + ], + status=500, + ) diff --git a/dlv_fleetflow_contactflow/models/__init__.py b/dlv_fleetflow_contactflow/models/__init__.py new file mode 100644 index 0000000..672127f --- /dev/null +++ b/dlv_fleetflow_contactflow/models/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import ir_http diff --git a/dlv_fleetflow_contactflow/models/ir_http.py b/dlv_fleetflow_contactflow/models/ir_http.py new file mode 100644 index 0000000..b1103c2 --- /dev/null +++ b/dlv_fleetflow_contactflow/models/ir_http.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +import logging + +from odoo import models +from odoo.http import request +from odoo.exceptions import AccessDenied +from werkzeug.exceptions import Unauthorized + +_logger = logging.getLogger(__name__) + + +class IrHttp(models.AbstractModel): + _inherit = "ir.http" + + @classmethod + def _auth_method_fleetflow_api_key(cls): + """Authenticate a request using a Bearer API key. + + The caller must provide an ``Authorization: Bearer `` header. + The key is validated against ``res.users.apikeys`` with the ``rpc`` + scope. On success ``request.uid`` is set to the owning user; on + failure a 401 Unauthorized is raised. + """ + authorization = request.httprequest.headers.get("Authorization", "") + if not authorization or not authorization.startswith("Bearer "): + _logger.warning( + "FleetFlow API: missing or malformed Authorization header" + ) + raise Unauthorized("Missing or invalid Authorization header") + + api_key = authorization[len("Bearer "):].strip() + if not api_key: + raise Unauthorized("Empty API key") + + try: + user_id = request.env["res.users.apikeys"]._check_credentials( + scope="rpc", key=api_key + ) + except AccessDenied: + user_id = False + + if not user_id: + _logger.warning("FleetFlow API: invalid API key") + raise Unauthorized("Invalid API key") + + request.uid = user_id