Files
enterprise-addons/pos_platform_order_grabfood/utils/grabfood_request.py
T

160 lines
6.6 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import requests
from typing import Any, Union
from requests.exceptions import RequestException
from urllib.parse import quote
from odoo import _, fields
from odoo.tools.urls import urljoin
from odoo.addons.pos_platform_order.models.platform_order_provider import PlatformOrderProvider
from odoo.addons.pos_platform_order.models.platform_order_entity import PlatformOrderEntity
_logger = logging.getLogger(__name__)
class GrabFoodClient:
BASE_OAUTH_URL = "https://api.grab.com"
BASE_API_URL_PROD = "https://partner-api.grab.com/grabfood/"
BASE_API_URL_SANDBOX = "https://partner-api.grab.com/grabfood-sandbox/"
TOKEN_ENDPOINT = "/grabid/v1/oauth2/token"
TOKEN_SCOPE = "food.partner_api"
GRANT_TYPE = "client_credentials"
JSON_HEADER = {"Content-Type": "application/json"}
def __init__(self, store: PlatformOrderEntity):
self.store = store
self.provider = store.provider_id
self.session = requests.Session()
self._prepare_session()
@property
def _api_base_url(self) -> str:
if self.provider.state == 'enabled':
return self.BASE_API_URL_PROD
return self.BASE_API_URL_SANDBOX
def _prepare_session(self) -> None:
"""
Validates the current access token and fetches a new one if expired.
Prepares the request session with the necessary headers.
"""
access_token = self.provider.grabfood_access_token
token_expiry = self.provider.grabfood_token_expiry
if not access_token or not token_expiry or token_expiry <= fields.Datetime.now():
access_token = self._fetch_and_store_new_token()
self.session.headers.update(self.JSON_HEADER)
self.session.headers["Authorization"] = f"Bearer {access_token}"
def _fetch_and_store_new_token(self) -> str:
_logger.info("Fetching new GrabFood access token for provider %s.", self.provider.id)
client_id = self.provider.grabfood_client_id
client_secret = self.provider.grabfood_client_secret
response = self._request_access_token(client_id, client_secret)
if response.status_code == 200:
response_json = response.json()
token = response_json['access_token']
self.provider._grabfood_handle_oauth_response(response_json)
return token
_logger.error("Failed to retrieve GrabFood access token: %s %s", response.status_code, response.text)
response.raise_for_status()
raise ValueError(_("GrabFood access token retrieval failed."))
def _make_api_request(self, method: str, endpoint: str, **kwargs: Any) -> Union[dict, list, None]:
url = urljoin(self._api_base_url, endpoint)
try:
response = self.session.request(method, url, **kwargs)
response.raise_for_status()
return response.json() if response.content else None
except RequestException as error:
_logger.warning('Connection Error: %r with the given URL %r', error, url)
raise
# --- PUBLIC API METHODS ---
def update_store_delivery_hours(self, service_hours: dict[str, list]) -> None:
endpoint = f"partner/v1/merchants/{quote(self.store.external_id)}/store/opening-hours"
payload = {"openingHour": service_hours, "force": False}
self._make_api_request("PUT", endpoint, json=payload)
def request_sync_menu(self) -> None:
endpoint = "partner/v1/merchant/menu/notification"
payload = {"merchantID": self.store.external_id}
self._make_api_request("POST", endpoint, json=payload)
def batch_update_menu(self, menu_items: list) -> None:
endpoint = "partner/v1/batch/menu"
payload = {
"merchantID": self.store.external_id,
"field": "ITEM",
"menuEntities": menu_items,
}
self._make_api_request("PUT", endpoint, json=payload)
def set_food_ready(self, order_ref, order_type) -> None:
endpoint = "partner/v1/orders/mark"
payload = {"orderID": order_ref, "markStatus": 2 if order_type == 'dine_in' else 1}
self._make_api_request("POST", endpoint, json=payload)
def update_order_state(self, order_ref, state) -> None:
endpoint = "partner/v1/order/prepare"
payload = {"orderID": order_ref, "toState": state}
self._make_api_request("POST", endpoint, json=payload)
def cancel_order(self, order_ref: str, reason_code: int):
endpoint = 'partner/v1/order/cancel'
payload = {"orderID": order_ref, "merchantID": self.store.external_id, "cancelCode": reason_code}
self._make_api_request("PUT", endpoint, json=payload)
# --- STATIC AND CLASS METHODS ---
@classmethod
def test_connection(cls, provider: PlatformOrderProvider):
client_id = provider.grabfood_client_id
client_secret = provider.grabfood_client_secret
try:
_logger.info("Testing GrabFood connection for provider ID %s", provider.id)
response = cls._request_access_token(client_id, client_secret)
if response.status_code == 200:
return {'success': True}
else:
error_message = f"Failed with status code {response.status_code}."
try:
error_details = response.json()
error_message += f" Error: {error_details.get('error_description', error_details.get('error', 'No details'))}"
except ValueError:
error_message += f" Response: {response.text}"
_logger.warning("GrabFood connection test failed: %s", error_message)
return {'success': False, 'message': error_message}
except RequestException as error:
_logger.error("GrabFood connection test encountered an error: %s", error)
raise
@staticmethod
def _request_access_token(client_id: str, client_secret: str) -> requests.Response:
url = urljoin(GrabFoodClient.BASE_OAUTH_URL, GrabFoodClient.TOKEN_ENDPOINT)
payload = {
"client_id": client_id,
"client_secret": client_secret,
"grant_type": GrabFoodClient.GRANT_TYPE,
"scope": GrabFoodClient.TOKEN_SCOPE,
}
try:
# Use a timeout for all external requests
response = requests.post(url, headers=GrabFoodClient.JSON_HEADER, json=payload, timeout=10)
return response
except RequestException as error:
_logger.error("GrabFood token request failed due to a network error: %s", error)
raise