Files
Odoo-Modules/fusion_poynt/models/poynt_terminal.py
gsinghpal e71bc503f9 changes
2026-02-25 09:40:41 -05:00

236 lines
7.8 KiB
Python

# 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_poynt import utils as poynt_utils
_logger = logging.getLogger(__name__)
class PoyntTerminal(models.Model):
_name = 'poynt.terminal'
_description = 'Poynt Terminal Device'
_order = 'name'
name = fields.Char(
string="Terminal Name",
required=True,
)
device_id = fields.Char(
string="Device ID",
help="The Poynt device identifier (urn:tid:...).",
required=True,
copy=False,
)
serial_number = fields.Char(
string="Serial Number",
copy=False,
)
provider_id = fields.Many2one(
'payment.provider',
string="Payment Provider",
required=True,
ondelete='cascade',
domain="[('code', '=', 'poynt')]",
)
store_id_poynt = fields.Char(
string="Poynt Store ID",
help="The Poynt store UUID this terminal belongs to.",
)
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_device_provider = models.Constraint(
'UNIQUE(device_id, provider_id)',
'A terminal with this device ID already exists for this provider.',
)
# === BUSINESS METHODS === #
def action_refresh_status(self):
"""Refresh the terminal status from Poynt Cloud."""
for terminal in self:
try:
store_id = terminal.store_id_poynt or terminal.provider_id.poynt_store_id
if store_id:
endpoint = f'stores/{store_id}/storeDevices/{terminal.device_id}'
else:
endpoint = f'storeDevices/{terminal.device_id}'
result = terminal.provider_id._poynt_make_request('GET', endpoint)
poynt_status = result.get('status', 'UNKNOWN')
if poynt_status == 'ACTIVATED':
terminal.status = 'online'
elif poynt_status in ('DEACTIVATED', 'INACTIVE'):
terminal.status = 'offline'
else:
terminal.status = 'unknown'
terminal.last_seen = fields.Datetime.now()
except (ValidationError, UserError) as e:
_logger.warning(
"Failed to refresh status for terminal %s: %s",
terminal.device_id, e,
)
terminal.status = 'unknown'
def action_send_payment_to_terminal(self, amount, currency, reference, order_id=None):
"""Push a payment request to the physical Poynt terminal.
This sends a cloud message to the terminal device instructing it
to start a payment collection for the given amount.
:param float amount: The payment amount in major currency units.
:param recordset currency: The currency record.
:param str reference: The Odoo payment reference.
:param str order_id: Optional Poynt order UUID to link.
:return: The Poynt cloud message response.
:rtype: dict
:raises UserError: If the terminal is offline.
"""
self.ensure_one()
if self.status == 'offline':
raise UserError(
_("Terminal '%(name)s' is offline. Please check the device.",
name=self.name)
)
minor_amount = poynt_utils.format_poynt_amount(amount, currency)
payment_request = {
'amount': minor_amount,
'currency': currency.name,
'referenceId': reference,
'callbackUrl': self._get_terminal_callback_url(),
'skipReceiptScreen': False,
'debit': True,
}
if order_id:
payment_request['orderId'] = order_id
store_id = self.store_id_poynt or self.provider_id.poynt_store_id or ''
data_str = json.dumps({
'action': 'sale',
'purchaseAmount': minor_amount,
'tipAmount': 0,
'currency': currency.name,
'referenceId': reference,
'callbackUrl': self._get_terminal_callback_url(),
})
try:
result = self.provider_id._poynt_make_request(
'POST',
'cloudMessages',
business_scoped=False,
payload={
'businessId': self.provider_id.poynt_business_id,
'storeId': store_id,
'deviceId': self.device_id,
'ttl': 300,
'serialNum': self.serial_number or '',
'data': data_str,
},
)
_logger.info(
"Payment request sent to terminal %s for %s %s (ref: %s)",
self.device_id, amount, currency.name, reference,
)
return result
except (ValidationError, UserError) as e:
_logger.error(
"Failed to send payment to terminal %s: %s",
self.device_id, e,
)
raise
def _get_terminal_callback_url(self):
"""Build the callback URL for terminal payment completion.
:return: The full callback URL.
:rtype: str
"""
base_url = self.provider_id.get_base_url()
return f"{base_url}/payment/poynt/terminal/callback"
def action_check_terminal_payment_status(self, reference):
"""Poll for the status of a terminal payment.
Searches Poynt transactions by referenceId (set via cloud message)
and falls back to notes field.
:param str reference: The Odoo transaction reference.
:return: Dict with status and transaction data if completed.
:rtype: dict
"""
self.ensure_one()
try:
txn_result = self.provider_id._poynt_make_request(
'GET',
'transactions',
params={
'referenceId': reference,
'limit': 5,
},
)
transactions = txn_result.get('transactions', [])
if not transactions:
txn_result = self.provider_id._poynt_make_request(
'GET',
'transactions',
params={
'notes': reference,
'limit': 5,
},
)
transactions = txn_result.get('transactions', [])
if not transactions:
return {'status': 'pending', 'message': 'Waiting for terminal response...'}
for txn in transactions:
status = txn.get('status', 'UNKNOWN')
if status in ('CAPTURED', 'AUTHORIZED', 'SETTLED'):
return {
'status': status,
'transaction_id': txn.get('id', ''),
'funding_source': txn.get('fundingSource', {}),
'amounts': txn.get('amounts', {}),
}
txn = transactions[0]
return {
'status': txn.get('status', 'UNKNOWN'),
'transaction_id': txn.get('id', ''),
'funding_source': txn.get('fundingSource', {}),
'amounts': txn.get('amounts', {}),
}
except (ValidationError, UserError):
return {'status': 'error', 'message': 'Failed to check payment status.'}