# Part of Odoo. See LICENSE file for full copyright and licensing details. import json import logging from odoo import _, api, fields, models from odoo.exceptions import UserError, ValidationError from odoo.addons.fusion_clover import utils as clover_utils _logger = logging.getLogger(__name__) class CloverTerminal(models.Model): _name = 'clover.terminal' _description = 'Clover Terminal Device' _order = 'name' name = fields.Char( string="Terminal Name", required=True, help="A friendly name for this terminal. You can rename it to " "identify the location (e.g. 'Front Desk', 'Back Office').", ) clover_device_name = fields.Char( string="Clover Device Name", readonly=True, help="The original device name from Clover (read-only).", ) serial_number = fields.Char( string="Serial Number", help="The Clover device serial number. Used as X-Clover-Device-Id header.", required=True, copy=False, ) device_id = fields.Char( string="Device ID", help="The Clover device UUID from the Platform API.", copy=False, ) provider_id = fields.Many2one( 'payment.provider', string="Payment Provider", required=True, ondelete='cascade', domain="[('code', '=', 'clover')]", ) model_name = fields.Char( string="Device Model", readonly=True, ) status = fields.Selection( selection=[ ('online', "Online"), ('offline', "Offline"), ('unknown', "Unknown"), ], string="Status", default='unknown', readonly=True, ) last_seen = fields.Datetime( string="Last Seen", readonly=True, ) active = fields.Boolean( default=True, ) _unique_serial_provider = models.Constraint( 'UNIQUE(serial_number, provider_id)', 'A terminal with this serial number already exists for this provider.', ) # === BUSINESS METHODS === # def _get_provider_sudo(self): return self.provider_id.sudo() def action_refresh_status(self): """Check terminal status via the Clover Platform API. First tries the Cloud Pay Display ping (POST /connect/v1/device/ping). If that fails (e.g. REST Pay Display not configured), falls back to the Platform API device endpoint (GET /v3/merchants/{mId}/devices/{deviceId}). """ self.ensure_one() provider = self._get_provider_sudo() # --- Attempt 1: Cloud Pay Display ping --- try: provider._clover_terminal_request( 'POST', 'device/ping', serial_number=self.serial_number, ) self.write({ 'status': 'online', 'last_seen': fields.Datetime.now(), }) return provider._clover_notification( _("Terminal '%(name)s' is online.", name=self.name), 'success', ) except (ValidationError, UserError): _logger.debug( "Cloud ping failed for %s, trying Platform API.", self.serial_number, ) # --- Attempt 2: Platform API device lookup --- if not self.device_id: self.status = 'unknown' return provider._clover_notification( _("Could not reach terminal '%(name)s'. " "Cloud Pay Display may not be configured for this merchant.", name=self.name), 'warning', ) try: result = provider._clover_make_platform_request( 'GET', f'devices/{self.device_id}', ) # Clover Platform API doesn't return real-time online/offline, # but a successful response means the device is registered. self.write({ 'status': 'online', 'last_seen': fields.Datetime.now(), }) return provider._clover_notification( _("Terminal '%(name)s' is registered and active on Clover.", name=self.name), 'success', ) except (ValidationError, UserError) as e: self.status = 'offline' return provider._clover_notification( _("Could not reach terminal '%(name)s': %(error)s", name=self.name, error=str(e)), 'danger', ) def action_send_payment(self, amount, currency, reference, capture=True): """Send a payment request to the Clover terminal via Cloud REST Pay API. :param float amount: The payment amount in major currency units. :param recordset currency: The currency record. :param str reference: The Odoo payment reference / externalPaymentId. :param bool capture: Whether to capture immediately (sale) or pre-auth. :return: The terminal payment response. :rtype: dict :raises UserError: If the terminal is offline. """ self.ensure_one() if self.status == 'offline': raise UserError( _("Terminal '%(name)s' appears to be offline. " "Please check the device and try again.", name=self.name) ) minor_amount = clover_utils.format_clover_amount(amount, currency) payload = { 'amount': minor_amount, 'externalPaymentId': reference, 'capture': capture, } provider = self._get_provider_sudo() result = provider._clover_terminal_request( 'POST', 'payments', serial_number=self.serial_number, payload=payload, ) _logger.info( "Payment request sent to terminal %s for %s %s (ref: %s)", self.serial_number, amount, currency.name, reference, ) return result def action_send_refund(self, payment_id, amount=None): """Send a refund request to the terminal. :param str payment_id: The Clover payment UUID to refund. :param int amount: Optional partial refund amount in cents. :return: The terminal refund response. :rtype: dict """ self.ensure_one() payload = {} if amount: payload['amount'] = amount else: payload['fullRefund'] = True provider = self._get_provider_sudo() return provider._clover_terminal_request( 'POST', f'payments/{payment_id}/refunds', serial_number=self.serial_number, payload=payload, ) def action_check_payment_status(self, external_payment_id): """Check the status of a terminal payment by externalPaymentId. :param str external_payment_id: The externalPaymentId sent with the payment. :return: Dict with status and payment data. :rtype: dict """ self.ensure_one() provider = self._get_provider_sudo() try: result = provider._clover_terminal_request( 'GET', f'payments?externalPaymentId={external_payment_id}', serial_number=self.serial_number, ) payment = result.get('payment', {}) if not payment: return {'status': 'pending', 'message': 'Waiting for terminal response...'} clover_result = payment.get('result', '') card_txn = payment.get('cardTransaction', {}) state = card_txn.get('state', '') if clover_result == 'SUCCESS': return { 'status': state or 'CLOSED', 'payment_id': payment.get('id', ''), 'card_transaction': card_txn, 'amount': payment.get('amount', 0), 'result': clover_result, } if clover_result in ('FAIL', 'DECLINED'): return { 'status': 'DECLINED', 'message': payment.get('failureMessage', 'Payment declined'), 'result': clover_result, } return { 'status': 'pending', 'message': f'Status: {clover_result or "processing"}', 'result': clover_result, } except (ValidationError, UserError): return {'status': 'error', 'message': 'Failed to check payment status.'} def action_display_welcome(self): """Reset the terminal to the welcome screen.""" self.ensure_one() provider = self._get_provider_sudo() try: provider._clover_terminal_request( 'POST', 'device/welcome', serial_number=self.serial_number, ) return provider._clover_notification( _("Welcome screen sent to '%(name)s'.", name=self.name), 'success', ) except (ValidationError, UserError) as e: _logger.warning("Failed to display welcome on terminal %s: %s", self.serial_number, e) return provider._clover_notification( _("Could not send welcome screen to '%(name)s': %(error)s", name=self.name, error=str(e)), 'danger', ) def _get_terminal_callback_url(self): """Build the callback URL for terminal payment completion.""" base_url = self._get_provider_sudo().get_base_url() return f"{base_url}/payment/clover/terminal/callback"