feat: add Pending status for delivery/technician tasks
- New 'pending' status allows tasks to be created without a schedule, acting as a queue for unscheduled work that gets assigned later - Pending group appears in the Delivery Map sidebar with amber color - Other modules can create tasks in pending state for scheduling - scheduled_date no longer required (null for pending tasks) - New Pending Tasks menu item under Field Service - Pending filter added to search view Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
386
fusion_poynt/models/payment_transaction.py
Normal file
386
fusion_poynt/models/payment_transaction.py
Normal file
@@ -0,0 +1,386 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
|
||||
from werkzeug.urls import url_encode
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools.urls import urljoin as url_join
|
||||
|
||||
from odoo.addons.fusion_poynt import const
|
||||
from odoo.addons.fusion_poynt import utils as poynt_utils
|
||||
from odoo.addons.fusion_poynt.controllers.main import PoyntController
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PaymentTransaction(models.Model):
|
||||
_inherit = 'payment.transaction'
|
||||
|
||||
poynt_order_id = fields.Char(
|
||||
string="Poynt Order ID",
|
||||
readonly=True,
|
||||
copy=False,
|
||||
)
|
||||
poynt_transaction_id = fields.Char(
|
||||
string="Poynt Transaction ID",
|
||||
readonly=True,
|
||||
copy=False,
|
||||
)
|
||||
|
||||
# === BUSINESS METHODS - PAYMENT FLOW === #
|
||||
|
||||
def _get_specific_processing_values(self, processing_values):
|
||||
"""Override of payment to return Poynt-specific processing values.
|
||||
|
||||
For direct (online) payments we create a Poynt order upfront and return
|
||||
identifiers plus the return URL so the frontend JS can complete the flow.
|
||||
"""
|
||||
if self.provider_code != 'poynt':
|
||||
return super()._get_specific_processing_values(processing_values)
|
||||
|
||||
if self.operation == 'online_token':
|
||||
return {}
|
||||
|
||||
poynt_data = self._poynt_create_order_and_authorize()
|
||||
|
||||
base_url = self.provider_id.get_base_url()
|
||||
return_url = url_join(
|
||||
base_url,
|
||||
f'{PoyntController._return_url}?{url_encode({"reference": self.reference})}',
|
||||
)
|
||||
|
||||
return {
|
||||
'poynt_order_id': poynt_data.get('order_id', ''),
|
||||
'poynt_transaction_id': poynt_data.get('transaction_id', ''),
|
||||
'return_url': return_url,
|
||||
'business_id': self.provider_id.poynt_business_id,
|
||||
'is_test': self.provider_id.state == 'test',
|
||||
}
|
||||
|
||||
def _send_payment_request(self):
|
||||
"""Override of `payment` to send a payment request to Poynt."""
|
||||
if self.provider_code != 'poynt':
|
||||
return super()._send_payment_request()
|
||||
|
||||
if self.operation in ('online_token', 'offline'):
|
||||
return self._poynt_process_token_payment()
|
||||
|
||||
poynt_data = self._poynt_create_order_and_authorize()
|
||||
if poynt_data:
|
||||
payment_data = {
|
||||
'reference': self.reference,
|
||||
'poynt_order_id': poynt_data.get('order_id'),
|
||||
'poynt_transaction_id': poynt_data.get('transaction_id'),
|
||||
'poynt_status': poynt_data.get('status', 'AUTHORIZED'),
|
||||
'funding_source': poynt_data.get('funding_source', {}),
|
||||
}
|
||||
self._process('poynt', payment_data)
|
||||
|
||||
def _poynt_create_order_and_authorize(self):
|
||||
"""Create a Poynt order and authorize the transaction.
|
||||
|
||||
:return: Dict with order_id, transaction_id, status, and funding_source.
|
||||
:rtype: dict
|
||||
"""
|
||||
try:
|
||||
order_payload = poynt_utils.build_order_payload(
|
||||
self.reference, self.amount, self.currency_id,
|
||||
)
|
||||
order_result = self.provider_id._poynt_make_request(
|
||||
'POST', 'orders', payload=order_payload,
|
||||
)
|
||||
order_id = order_result.get('id', '')
|
||||
self.poynt_order_id = order_id
|
||||
|
||||
action = 'AUTHORIZE' if self.provider_id.capture_manually else 'SALE'
|
||||
txn_payload = poynt_utils.build_transaction_payload(
|
||||
action=action,
|
||||
amount=self.amount,
|
||||
currency=self.currency_id,
|
||||
order_id=order_id,
|
||||
reference=self.reference,
|
||||
)
|
||||
txn_result = self.provider_id._poynt_make_request(
|
||||
'POST', 'transactions', payload=txn_payload,
|
||||
)
|
||||
|
||||
transaction_id = txn_result.get('id', '')
|
||||
self.poynt_transaction_id = transaction_id
|
||||
self.provider_reference = transaction_id
|
||||
|
||||
return {
|
||||
'order_id': order_id,
|
||||
'transaction_id': transaction_id,
|
||||
'status': txn_result.get('status', ''),
|
||||
'funding_source': txn_result.get('fundingSource', {}),
|
||||
}
|
||||
except ValidationError as e:
|
||||
self._set_error(str(e))
|
||||
return {}
|
||||
|
||||
def _poynt_process_token_payment(self):
|
||||
"""Process a payment using a stored token (card on file).
|
||||
|
||||
For token-based payments we send a SALE or AUTHORIZE using the
|
||||
stored card ID from the payment token.
|
||||
"""
|
||||
try:
|
||||
action = 'AUTHORIZE' if self.provider_id.capture_manually else 'SALE'
|
||||
|
||||
funding_source = {
|
||||
'type': 'CREDIT_DEBIT',
|
||||
'card': {
|
||||
'cardId': self.token_id.poynt_card_id,
|
||||
},
|
||||
}
|
||||
|
||||
order_payload = poynt_utils.build_order_payload(
|
||||
self.reference, self.amount, self.currency_id,
|
||||
)
|
||||
order_result = self.provider_id._poynt_make_request(
|
||||
'POST', 'orders', payload=order_payload,
|
||||
)
|
||||
order_id = order_result.get('id', '')
|
||||
self.poynt_order_id = order_id
|
||||
|
||||
txn_payload = poynt_utils.build_transaction_payload(
|
||||
action=action,
|
||||
amount=self.amount,
|
||||
currency=self.currency_id,
|
||||
order_id=order_id,
|
||||
reference=self.reference,
|
||||
funding_source=funding_source,
|
||||
)
|
||||
txn_result = self.provider_id._poynt_make_request(
|
||||
'POST', 'transactions', payload=txn_payload,
|
||||
)
|
||||
|
||||
transaction_id = txn_result.get('id', '')
|
||||
self.poynt_transaction_id = transaction_id
|
||||
self.provider_reference = transaction_id
|
||||
|
||||
payment_data = {
|
||||
'reference': self.reference,
|
||||
'poynt_order_id': order_id,
|
||||
'poynt_transaction_id': transaction_id,
|
||||
'poynt_status': txn_result.get('status', ''),
|
||||
'funding_source': txn_result.get('fundingSource', {}),
|
||||
}
|
||||
self._process('poynt', payment_data)
|
||||
except ValidationError as e:
|
||||
self._set_error(str(e))
|
||||
|
||||
def _send_refund_request(self):
|
||||
"""Override of `payment` to send a refund request to Poynt."""
|
||||
if self.provider_code != 'poynt':
|
||||
return super()._send_refund_request()
|
||||
|
||||
source_tx = self.source_transaction_id
|
||||
refund_amount = abs(self.amount)
|
||||
minor_amount = poynt_utils.format_poynt_amount(refund_amount, self.currency_id)
|
||||
|
||||
try:
|
||||
refund_payload = {
|
||||
'action': 'REFUND',
|
||||
'parentId': source_tx.provider_reference,
|
||||
'amounts': {
|
||||
'transactionAmount': minor_amount,
|
||||
'orderAmount': minor_amount,
|
||||
'currency': self.currency_id.name,
|
||||
},
|
||||
'context': {
|
||||
'source': 'WEB',
|
||||
'sourceApp': 'odoo.fusion_poynt',
|
||||
},
|
||||
'notes': f'Refund for {source_tx.reference}',
|
||||
}
|
||||
|
||||
result = self.provider_id._poynt_make_request(
|
||||
'POST', 'transactions', payload=refund_payload,
|
||||
)
|
||||
|
||||
self.provider_reference = result.get('id', '')
|
||||
self.poynt_transaction_id = result.get('id', '')
|
||||
|
||||
payment_data = {
|
||||
'reference': self.reference,
|
||||
'poynt_transaction_id': result.get('id'),
|
||||
'poynt_status': result.get('status', 'REFUNDED'),
|
||||
}
|
||||
self._process('poynt', payment_data)
|
||||
except ValidationError as e:
|
||||
self._set_error(str(e))
|
||||
|
||||
def _send_capture_request(self):
|
||||
"""Override of `payment` to send a capture request to Poynt."""
|
||||
if self.provider_code != 'poynt':
|
||||
return super()._send_capture_request()
|
||||
|
||||
source_tx = self.source_transaction_id
|
||||
minor_amount = poynt_utils.format_poynt_amount(self.amount, self.currency_id)
|
||||
|
||||
try:
|
||||
capture_payload = {
|
||||
'action': 'CAPTURE',
|
||||
'parentId': source_tx.provider_reference,
|
||||
'amounts': {
|
||||
'transactionAmount': minor_amount,
|
||||
'orderAmount': minor_amount,
|
||||
'currency': self.currency_id.name,
|
||||
},
|
||||
'context': {
|
||||
'source': 'WEB',
|
||||
'sourceApp': 'odoo.fusion_poynt',
|
||||
},
|
||||
}
|
||||
|
||||
result = self.provider_id._poynt_make_request(
|
||||
'POST', 'transactions', payload=capture_payload,
|
||||
)
|
||||
|
||||
payment_data = {
|
||||
'reference': self.reference,
|
||||
'poynt_transaction_id': result.get('id'),
|
||||
'poynt_status': result.get('status', 'CAPTURED'),
|
||||
}
|
||||
self._process('poynt', payment_data)
|
||||
except ValidationError as e:
|
||||
self._set_error(str(e))
|
||||
|
||||
def _send_void_request(self):
|
||||
"""Override of `payment` to send a void request to Poynt."""
|
||||
if self.provider_code != 'poynt':
|
||||
return super()._send_void_request()
|
||||
|
||||
source_tx = self.source_transaction_id
|
||||
|
||||
try:
|
||||
void_payload = {
|
||||
'action': 'VOID',
|
||||
'parentId': source_tx.provider_reference,
|
||||
'context': {
|
||||
'source': 'WEB',
|
||||
'sourceApp': 'odoo.fusion_poynt',
|
||||
},
|
||||
}
|
||||
|
||||
result = self.provider_id._poynt_make_request(
|
||||
'POST', 'transactions', payload=void_payload,
|
||||
)
|
||||
|
||||
payment_data = {
|
||||
'reference': self.reference,
|
||||
'poynt_transaction_id': result.get('id'),
|
||||
'poynt_status': result.get('status', 'VOIDED'),
|
||||
}
|
||||
self._process('poynt', payment_data)
|
||||
except ValidationError as e:
|
||||
self._set_error(str(e))
|
||||
|
||||
# === BUSINESS METHODS - NOTIFICATION PROCESSING === #
|
||||
|
||||
@api.model
|
||||
def _search_by_reference(self, provider_code, payment_data):
|
||||
"""Override of payment to find the transaction based on Poynt data."""
|
||||
if provider_code != 'poynt':
|
||||
return super()._search_by_reference(provider_code, payment_data)
|
||||
|
||||
reference = payment_data.get('reference')
|
||||
if reference:
|
||||
tx = self.search([
|
||||
('reference', '=', reference),
|
||||
('provider_code', '=', 'poynt'),
|
||||
])
|
||||
else:
|
||||
poynt_txn_id = payment_data.get('poynt_transaction_id')
|
||||
if poynt_txn_id:
|
||||
tx = self.search([
|
||||
('poynt_transaction_id', '=', poynt_txn_id),
|
||||
('provider_code', '=', 'poynt'),
|
||||
])
|
||||
else:
|
||||
_logger.warning("Received Poynt data with no reference or transaction ID")
|
||||
tx = self
|
||||
|
||||
if not tx:
|
||||
_logger.warning(
|
||||
"No transaction found matching Poynt reference %s", reference,
|
||||
)
|
||||
|
||||
return tx
|
||||
|
||||
def _apply_updates(self, payment_data):
|
||||
"""Override of `payment` to update the transaction based on Poynt data."""
|
||||
if self.provider_code != 'poynt':
|
||||
return super()._apply_updates(payment_data)
|
||||
|
||||
poynt_txn_id = payment_data.get('poynt_transaction_id')
|
||||
if poynt_txn_id:
|
||||
self.provider_reference = poynt_txn_id
|
||||
self.poynt_transaction_id = poynt_txn_id
|
||||
|
||||
poynt_order_id = payment_data.get('poynt_order_id')
|
||||
if poynt_order_id:
|
||||
self.poynt_order_id = poynt_order_id
|
||||
|
||||
funding_source = payment_data.get('funding_source', {})
|
||||
if funding_source:
|
||||
card_details = poynt_utils.extract_card_details(funding_source)
|
||||
if card_details.get('brand'):
|
||||
payment_method = self.env['payment.method']._get_from_code(
|
||||
card_details['brand'],
|
||||
mapping=const.CARD_BRAND_MAPPING,
|
||||
)
|
||||
if payment_method:
|
||||
self.payment_method_id = payment_method
|
||||
|
||||
status = payment_data.get('poynt_status', '')
|
||||
if not status:
|
||||
self._set_error(_("Received data with missing transaction status."))
|
||||
return
|
||||
|
||||
odoo_state = poynt_utils.get_poynt_status(status)
|
||||
|
||||
if odoo_state == 'authorized':
|
||||
self._set_authorized()
|
||||
elif odoo_state == 'done':
|
||||
self._set_done()
|
||||
if self.operation == 'refund':
|
||||
self.env.ref('payment.cron_post_process_payment_tx')._trigger()
|
||||
elif odoo_state == 'cancel':
|
||||
self._set_canceled()
|
||||
elif odoo_state == 'refund':
|
||||
self._set_done()
|
||||
self.env.ref('payment.cron_post_process_payment_tx')._trigger()
|
||||
elif odoo_state == 'error':
|
||||
error_msg = payment_data.get('error_message', _("Payment was declined by Poynt."))
|
||||
self._set_error(error_msg)
|
||||
else:
|
||||
_logger.warning(
|
||||
"Received unknown Poynt status (%s) for transaction %s.",
|
||||
status, self.reference,
|
||||
)
|
||||
self._set_error(
|
||||
_("Received data with unrecognized status: %s.", status)
|
||||
)
|
||||
|
||||
def _extract_token_values(self, payment_data):
|
||||
"""Override of `payment` to return token data based on Poynt data."""
|
||||
if self.provider_code != 'poynt':
|
||||
return super()._extract_token_values(payment_data)
|
||||
|
||||
funding_source = payment_data.get('funding_source', {})
|
||||
card_details = poynt_utils.extract_card_details(funding_source)
|
||||
|
||||
if not card_details:
|
||||
_logger.warning(
|
||||
"Tokenization requested but no card data in payment response."
|
||||
)
|
||||
return {}
|
||||
|
||||
return {
|
||||
'payment_details': card_details.get('last4', ''),
|
||||
'poynt_card_id': card_details.get('card_id', ''),
|
||||
}
|
||||
Reference in New Issue
Block a user