Files
gsinghpal a2fe1fcbcc changes
2026-04-29 03:35:33 -04:00

200 lines
6.8 KiB
Python

# 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 ``/v1/refunds`` payload.
The Clover Ecommerce ``/v1/refunds`` endpoint accepts ONLY the
``charge`` field — including any other field triggers a 400
"Invalid JSON format" response. ``amount`` and ``reason`` arguments
are accepted for backwards-compat but ignored by this endpoint.
For partial refunds, callers should use the
``/v1/payments/{paymentId}/refunds`` endpoint instead, which takes
``{"amount": <cents>}`` or ``{"fullRefund": true}``.
https://docs.clover.com/dev/docs/ecommerce-refunding-payments
:param str charge_id: The Clover charge ID to refund.
:param float amount: IGNORED on this endpoint (full refund only).
Use a /payments/{id}/refunds call for partial.
:param recordset currency: IGNORED.
:param str reason: IGNORED on this endpoint.
:return: The Clover-formatted refund payload.
:rtype: dict
"""
return {'charge': charge_id}
def build_payment_refund_payload(amount=None, currency=None, full_refund=False):
"""Build a Clover ``/v1/payments/{paymentId}/refunds`` payload.
Used for partial refunds (the ``/v1/refunds`` endpoint is full-refund
only). Either pass ``full_refund=True`` to refund the entire payment,
or ``amount`` (with ``currency``) for a partial.
:param float amount: Partial refund amount in major currency units.
:param recordset currency: Currency record for the partial refund.
:param bool full_refund: If True, refund the entire payment.
:return: The Clover-formatted payment-refund payload.
:rtype: dict
"""
if full_refund:
return {'fullRefund': True}
if amount is not None and currency:
return {'amount': format_clover_amount(amount, currency)}
return {'fullRefund': True}