236 lines
7.8 KiB
Python
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.'}
|