167 lines
7.3 KiB
Python
167 lines
7.3 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from pprint import pformat
|
|
from typing import Literal, TYPE_CHECKING
|
|
from urllib.parse import quote
|
|
|
|
import requests
|
|
from requests.exceptions import RequestException
|
|
|
|
from odoo.tools.urls import urljoin
|
|
|
|
if TYPE_CHECKING:
|
|
from odoo.addons.pos_platform_order_gofood.utils.typing import GoFoodResponse
|
|
from odoo.addons.pos_platform_order.models.platform_order_entity import PlatformOrderEntity
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class GoFoodClient:
|
|
|
|
def __init__(self, store: PlatformOrderEntity):
|
|
self.store = store
|
|
self.provider = store.provider_id
|
|
self.base_url = store.env.company.get_base_url()
|
|
|
|
self.session = requests.Session()
|
|
self.session.headers.update({
|
|
'Content-Type': 'application/json',
|
|
})
|
|
|
|
def _get_api_url(self):
|
|
return "https://api.gobiz.co.id" if self.store.provider_id.state == 'enabled' else "https://api.partner-sandbox.gobiz.co.id"
|
|
|
|
def _get_oauth_url(self):
|
|
return "https://accounts.go-jek.com" if self.store.provider_id.state == 'enabled' else "https://integration-goauth.gojekapi.com"
|
|
|
|
def _fetch_access_token(self):
|
|
if 'Authorization' in self.session.headers:
|
|
return
|
|
|
|
scopes = 'partner:outlet:read partner:outlet:write gofood:outlet:write gofood:catalog:read gofood:catalog:write gofood:order:read gofood:order:write'
|
|
access_url = urljoin(self._get_oauth_url(), '/oauth2/token')
|
|
appid = self.store.provider_id.gofood_appid
|
|
secret = self.store.provider_id.gofood_secret
|
|
response = self.session.post(access_url, auth=(appid, secret), data={
|
|
'grant_type': 'client_credentials',
|
|
'scope': scopes,
|
|
}, headers={
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
})
|
|
|
|
if response.status_code == 200:
|
|
token_data = response.json()
|
|
self.session.headers['Authorization'] = 'Bearer ' + token_data['access_token']
|
|
else:
|
|
_logger.error('Failed to fetch access token: %s', response.text)
|
|
|
|
def _make_api_request(self, endpoint, method='POST', data=None, timeout=10) -> GoFoodResponse:
|
|
"""
|
|
Make an api call, return response for multiple api requests of gofood.
|
|
"""
|
|
self._fetch_access_token()
|
|
url = self._get_api_url() + endpoint
|
|
|
|
try:
|
|
response = self.session.request(method, url, json=data, timeout=timeout)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
except RequestException as error:
|
|
_logger.warning('Connection Error: %r with the given URL %r', error, url)
|
|
raise
|
|
|
|
def request_sync_menu(self, payload) -> GoFoodResponse:
|
|
"""
|
|
Sync menu in GoFood.
|
|
- Sync categories, products, attributes, values and taxes.
|
|
- Note: This will fail to sync menu in localhost because GoFood couldn't fetch the food images.
|
|
"""
|
|
self._configure_webhook()
|
|
endpoint = f'/integrations/gofood/outlets/{quote(self.store.external_id)}/v1/catalog'
|
|
|
|
return self._make_api_request(endpoint, method='PUT', data=payload, timeout=90)
|
|
|
|
def _configure_webhook(self):
|
|
"""
|
|
Check and register webhook if the base url is changed.
|
|
"""
|
|
endpoint = f"/integrations/partner/outlets/{quote(self.store.external_id)}/v1/notification-subscriptions"
|
|
response_json = self._make_api_request(endpoint, method='GET')
|
|
|
|
data = response_json.get('data') or {}
|
|
existing_webhooks = data.get('subscriptions', [])
|
|
|
|
base_events = ['awaiting_merchant_acceptance', 'merchant_accepted', 'driver_otw_pickup', 'driver_arrived', 'placed', 'completed', 'cancelled']
|
|
required_events = ['gofood.order.' + event for event in base_events]
|
|
|
|
controller_url = urljoin(self.base_url, '/gofood/notification')
|
|
|
|
existing_events_map = {webhook.get('event'): webhook for webhook in existing_webhooks if webhook.get('event')}
|
|
|
|
for event, webhook in existing_events_map.items():
|
|
url = webhook.get('url')
|
|
if not url or url != controller_url:
|
|
_logger.info("Webhook URL for event %s does not match expected URL %s, updating...", event, controller_url)
|
|
try:
|
|
self._update_webhook(webhook)
|
|
except RequestException as e:
|
|
_logger.error("Failed to update webhook for %s: %s", event, e)
|
|
|
|
missing_events = [event for event in required_events if event not in existing_events_map]
|
|
|
|
if missing_events:
|
|
_logger.info("Missing webhooks found for events: %s. Registering them...", missing_events)
|
|
self._register_webhooks(missing_events)
|
|
else:
|
|
_logger.info("All GoFood webhooks are fully configured.")
|
|
|
|
def _register_webhooks(self, events_to_register):
|
|
endpoint = f"/integrations/partner/outlets/{quote(self.store.external_id)}/v1/notification-subscriptions"
|
|
controller_url = urljoin(self.base_url, '/gofood/notification')
|
|
|
|
for event in events_to_register:
|
|
payload = {
|
|
'event': event,
|
|
'url': controller_url,
|
|
'active': True,
|
|
}
|
|
try:
|
|
response = self._make_api_request(endpoint, method='POST', data=payload)
|
|
_logger.info("Event (%s) webhook successfully registered. Response: %s", event, pformat(response))
|
|
except RequestException as e:
|
|
_logger.error("Failed to register webhook for event %s: %s", event, e)
|
|
|
|
def _update_webhook(self, webhook):
|
|
endpoint = f"/integrations/partner/outlets/{quote(self.store.external_id)}/v1/notification-subscriptions/{webhook['id']}"
|
|
payload = {
|
|
'event': webhook['event'],
|
|
'url': urljoin(self.base_url, '/gofood/notification'),
|
|
'active': True,
|
|
}
|
|
return self._make_api_request(endpoint, method='PUT', data=payload)
|
|
|
|
def set_food_ready(self, order_ref):
|
|
endpoint = f'/integrations/gofood/outlets/{quote(self.store.external_id)}/v1/orders/delivery/{order_ref}/food-prepared'
|
|
payload = {
|
|
'country_code': self.store.company_id.country_code,
|
|
}
|
|
return self._make_api_request(endpoint, method='PUT', data=payload)
|
|
|
|
def accept_order(self, order_ref):
|
|
endpoint = f'/integrations/gofood/outlets/{quote(self.store.external_id)}/v1/orders/delivery/{order_ref}/accepted' # Only delivery is supported
|
|
return self._make_api_request(endpoint, method='PUT')
|
|
|
|
def reject_order(self, order_ref: str, cancel_reason_code: Literal["HIGH_DEMAND", "RESTAURANT_CLOSED", "ITEMS_OUT_OF_STOCK", "OTHERS"], cancel_reason_description: str):
|
|
endpoint = f'/integrations/gofood/outlets/{self.store.external_id}/v1/orders/delivery/{order_ref}/cancelled'
|
|
payload = {
|
|
"cancel_reason_code": cancel_reason_code,
|
|
"cancel_reason_description": cancel_reason_description, # Min length = 3
|
|
}
|
|
return self._make_api_request(endpoint, method='PUT', data=payload)
|
|
|
|
def batch_update_menu(self, payload):
|
|
endpoint = f'/integrations/gofood/outlets/{quote(self.store.external_id)}/v2/menu_item_stocks'
|
|
return self._make_api_request(endpoint, method='PATCH', data=payload)
|