from odoo import http, fields from odoo.http import request import json import logging _logger = logging.getLogger(__name__) class UberWebhookController(http.Controller): @http.route('/uber/webhook/delivery', type='http', auth='public', methods=['POST'], csrf=False) def uber_delivery_webhook(self, **post): """Handle status updates from Uber Direct via standard HTTP webhook""" try: raw_data = request.httprequest.get_data(as_text=True) data = json.loads(raw_data) if raw_data else {} except Exception as e: _logger.error("Uber Webhook invalid JSON: %s", str(e)) return request.make_json_response({'status': 'error', 'message': 'Invalid JSON'}, status=400) _logger.info("Uber Webhook Received: %s", json.dumps(data, indent=2)) uber_delivery_id = ( data.get('delivery_id') or data.get('id') or (data.get('data') and data['data'].get('id')) or (data.get('meta') and data['meta'].get('resource_id')) ) status = ( data.get('status') or (data.get('data') and data['data'].get('status')) or data.get('event_type', '').split('.')[-1] ) if uber_delivery_id: order = request.env['pos.order'].sudo().search([('uber_delivery_id', '=', uber_delivery_id)], limit=1) if order: # Map Uber status to Odoo status status_map = { 'pickup': 'pickup', 'pickup_ready': 'pickup', 'courier_assigned': 'pickup', 'pickup_completed': 'delivering', 'in_transit': 'delivering', 'dropoff_completed': 'delivered', 'delivered': 'delivered', 'cancelled': 'cancelled', 'canceled': 'cancelled' } new_status = status_map.get(status) if new_status and new_status != order.uber_status: vals = {'uber_status': new_status} if new_status != 'pending': vals['uber_alert_triggered'] = False order.write(vals) request.env['bus.bus'].sudo()._sendone('uber_status_updates', 'status_changed', { 'order_id': order.id, 'new_status': new_status }) return request.make_json_response({'status': 'success'}) return request.make_json_response({'status': 'ignored'}) class UberDeliveryController(http.Controller): @http.route('/shop/uber/quote', type='json', auth='public', website=True, csrf=False) def uber_quote(self, address_data, **post): """Get Uber quote for a website address with cleaned address formatting and boundary validation""" order = request.website.sale_get_order() if not order: return {'success': False, 'error': 'No active order'} config = request.env['uber.config'].sudo().search([('active', '=', True)], limit=1) if not config: return {'success': False, 'error': 'Uber delivery is not configured'} company = request.website.company_id company_country = company.country_id # User entered address fields street = (address_data.get('street') or '').strip() street2 = (address_data.get('street2') or '').strip() full_street = f"{street}, {street2}" if street2 else street city = (address_data.get('city') or '').strip() zip_code = (address_data.get('zip') or '').strip() country_name = (address_data.get('country') or '').strip() country_id = address_data.get('country_id') state_input = (address_data.get('state') or '').split('(')[0].strip() # Resolve customer country customer_country = None if country_id: try: customer_country = request.env['res.country'].sudo().browse(int(country_id)) except Exception: pass if not customer_country and country_name: customer_country = request.env['res.country'].sudo().search([ '|', ('name', '=ilike', country_name), ('code', '=ilike', country_name) ], limit=1) # 1. Geographic Boundary Check: Customer Country vs Restaurant Operating Country if customer_country and company_country and customer_country.id != company_country.id: _logger.warning("Delivery rejected: Customer country '%s' does not match company country '%s'", customer_country.name, company_country.name) order.sudo()._remove_uber_delivery_fee() return { 'success': False, 'error': f"Delivery is not available outside {company_country.name}. Please select Store Pickup or enter a Canadian address." } if country_name and company_country: if country_name.lower() not in [company_country.name.lower(), company_country.code.lower()]: _logger.warning("Delivery rejected: Country name '%s' does not match '%s'", country_name, company_country.name) order.sudo()._remove_uber_delivery_fee() return { 'success': False, 'error': f"Delivery is not available outside {company_country.name}. Please select Store Pickup or enter a Canadian address." } # Resolve State state_code = company.state_id.code if company.state_id else "ON" if company_country: state_record = request.env['res.country.state'].sudo().search([ ('country_id', '=', company_country.id), '|', ('name', '=ilike', state_input), ('code', '=ilike', state_input) ], limit=1) if state_record: state_code = state_record.code # Build STRUCTURED pickup address dynamically from Company in Settings company_street = [s.strip() for s in [company.street, company.street2] if s and s.strip()] pickup_address = { "street_address": company_street if company_street else [company.name], "city": (company.city or "").strip(), "state": company.state_id.code if company.state_id else "", "zip_code": (company.zip or "").strip(), "country": company.country_id.code if company.country_id else "CA" } # Build STRUCTURED dropoff address (Object) dropoff_address = { "street_address": [full_street], "city": city, "state": state_code, "zip_code": zip_code, "country": company.country_id.code or "CA" } # Structured dict for distance calculator dropoff_calc_data = { 'street': street, 'street2': street2, 'city': city, 'state': state_code, 'zip': zip_code, 'country': customer_country.name if customer_country else (country_name or (company_country.name if company_country else 'Canada')) } # For logging, create strings p_str = f"{', '.join(pickup_address['street_address'])}, {pickup_address['city']} {pickup_address['state']} {pickup_address['zip_code']}" d_str = f"{dropoff_address['street_address'][0]}, {dropoff_address['city']} {dropoff_address['state']} {dropoff_address['zip_code']}" _logger.info("WEBSITE UBER QUOTE -\nPickup: [%s]\nDropoff: [%s, %s]", p_str, d_str, dropoff_calc_data['country']) # POS ENCODING: The POS sends these as JSON-encoded STRINGS result = config.get_uber_quote( json.dumps(pickup_address), json.dumps(dropoff_address), dropoff_data=dropoff_calc_data, company=company ) if result.get('success'): order.sudo()._add_uber_delivery_fee(result['fee_amount']) return { 'success': True, 'fee': result['fee_amount'], 'distance_km': result.get('distance_km'), 'eta': result.get('estimated_arrival'), 'is_fallback': result.get('is_fallback', False), 'is_distance_based': result.get('is_distance_based', False), 'is_uber_direct': result.get('is_uber_direct', False), } else: _logger.warning("Uber Quote Rejected/Failed: %s", result.get('error')) order.sudo()._remove_uber_delivery_fee() return result class UberPolicyController(http.Controller): @http.route(['/privacy', '/privacy-policy'], type='http', auth='public', website=True, sitemap=True) def privacy_policy(self, **kwargs): """Render restaurant delivery privacy policy compliant with Uber Direct policies""" company = request.website.company_id if hasattr(request, 'website') and request.website else request.env.company today_str = fields.Date.today().strftime('%B %d, %Y') return request.render('dine360_uber.privacy_policy_page', { 'company': company, 'current_date': today_str, }) @http.route(['/uber/callback', '/uber/auth/callback'], type='http', auth='public', website=True, csrf=False) def uber_callback(self, **kwargs): """Handle Uber OAuth callback redirect""" return request.render('dine360_uber.uber_oauth_callback_page', {})