# Part of Odoo. See LICENSE file for full copyright and licensing details. import uuid from odoo.addons.fusion_clover import const def generate_idempotency_key(): """Generate a unique idempotency key for Clover API requests.""" return str(uuid.uuid4()) def build_ecom_url(endpoint, is_test=False): """Build a full Clover Ecommerce API URL. :param str endpoint: The API endpoint path (e.g., 'v1/charges'). :param bool is_test: Whether to use the sandbox environment. :return: The full API URL. :rtype: str """ base = const.ECOM_BASE_URL_TEST if is_test else const.ECOM_BASE_URL return f"{base}/{endpoint}" def build_platform_url(endpoint, merchant_id=None, is_test=False): """Build a full Clover Platform API URL. :param str endpoint: The API endpoint path. :param str merchant_id: The merchant ID (optional). :param bool is_test: Whether to use the sandbox environment. :return: The full API URL. :rtype: str """ base = const.API_BASE_URL_TEST if is_test else const.API_BASE_URL if merchant_id: return f"{base}/v3/merchants/{merchant_id}/{endpoint}" return f"{base}/{endpoint}" def build_ecom_headers(api_key, idempotency_key=None): """Build the standard HTTP headers for a Clover Ecommerce API request. :param str api_key: The Clover API key (Bearer token). :param str idempotency_key: Optional unique key for idempotency. :return: The request headers dict. :rtype: dict """ headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': f'Bearer {api_key}', } if idempotency_key: headers['idempotency-key'] = idempotency_key return headers def format_clover_amount(amount, currency): """Convert a major currency amount to Clover's minor units (cents). :param float amount: The amount in major currency units. :param recordset currency: The currency record. :return: The amount in minor currency units (integer). :rtype: int """ decimals = const.CURRENCY_DECIMALS.get(currency.name, 2) return int(round(amount * (10 ** decimals))) def parse_clover_amount(minor_amount, currency): """Convert Clover's minor currency units back to major units. :param int minor_amount: The amount in minor currency units. :param recordset currency: The currency record. :return: The amount in major currency units. :rtype: float """ decimals = const.CURRENCY_DECIMALS.get(currency.name, 2) return minor_amount / (10 ** decimals) def extract_card_details(source): """Extract card details from a Clover charge source object. :param dict source: The Clover source object from a charge response. :return: Dict with card brand, last4, expiration. :rtype: dict """ if not source: return {} brand_raw = source.get('brand', '') brand_code = const.CARD_BRAND_MAPPING.get(brand_raw, 'card') return { 'brand': brand_code, 'last4': str(source.get('last4', '')), 'exp_month': source.get('exp_month'), 'exp_year': source.get('exp_year'), 'first6': str(source.get('first6', '')), } def get_clover_status(status_str): """Map a Clover charge status string to an Odoo transaction state. :param str status_str: The Clover charge status. :return: The corresponding Odoo payment state. :rtype: str """ for odoo_state, clover_statuses in const.STATUS_MAPPING.items(): if status_str in clover_statuses: return odoo_state return 'error' def build_charge_payload(amount, currency, source_token, capture=True, description='', ecomind='ecom', external_reference_id='', receipt_email='', metadata=None): """Build a Clover charge creation payload. :param float amount: The charge amount in major currency units. :param recordset currency: The currency record. :param str source_token: The Clover card token. :param bool capture: Whether to capture immediately (True) or pre-auth (False). :param str description: Optional charge description. :param str ecomind: 'ecom' for customer-initiated, 'moto' for merchant-initiated. :param str external_reference_id: External reference (max 12 chars). :param str receipt_email: Email to send receipt to. :param dict metadata: Optional key-value metadata. :return: The Clover-formatted charge payload. :rtype: dict """ minor_amount = format_clover_amount(amount, currency) payload = { 'amount': minor_amount, 'currency': currency.name.lower(), 'source': source_token, 'capture': capture, 'ecomind': ecomind, } if description: payload['description'] = description if external_reference_id: payload['external_reference_id'] = external_reference_id[:12] if receipt_email: payload['receipt_email'] = receipt_email if metadata: payload['metadata'] = metadata return payload def build_refund_payload(charge_id, amount=None, currency=None, reason=''): """Build a Clover refund payload. :param str charge_id: The Clover charge ID to refund. :param float amount: Optional partial refund amount in major currency units. :param recordset currency: Optional currency record (needed for partial refunds). :param str reason: Optional reason for the refund. :return: The Clover-formatted refund payload. :rtype: dict """ payload = { 'charge': charge_id, } if amount is not None and currency: payload['amount'] = format_clover_amount(amount, currency) if reason: payload['reason'] = reason return payload