changes
This commit is contained in:
282
fusion_clover/models/clover_terminal.py
Normal file
282
fusion_clover/models/clover_terminal.py
Normal file
@@ -0,0 +1,282 @@
|
||||
# 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"
|
||||
Reference in New Issue
Block a user