[FIX] pos_{adyen,self_order{,_adyen}}: permissions
- Adds some permissions to protect sensitive pos_adyen data and methods. - Reduces the risks of abuses of the pos_adyen features. Those changes are not optimal. The whole system should be changed. - Fixes the printing of Adyen receipts data in the receipt. - Fixes an error when receiving a successful Adyen payment notification for the kiosk. - Fixes an error when Adyen returns an error for a payment for kiosk. - Fixes an error when requesting adjustment/capture to Adyen (pos_restaurant_adyen and kiosk). - Removes unused handling of adyen_latest_diagnosis field (the field will be removed in master). closes odoo/odoo#140327 Task-id: 3500598 Signed-off-by: Théo Vincent (thvi) <thvi@odoo.com>
This commit is contained in:
@@ -2,14 +2,17 @@
|
||||
import logging
|
||||
import pprint
|
||||
import json
|
||||
from urllib.parse import parse_qs
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.tools import consteq
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PosAdyenController(http.Controller):
|
||||
@http.route('/pos_adyen/notification', type='json', methods=['POST'], auth='none', csrf=False)
|
||||
|
||||
@http.route('/pos_adyen/notification', type='json', methods=['POST'], auth='public', csrf=False, save_session=False)
|
||||
def notification(self):
|
||||
data = json.loads(request.httprequest.data)
|
||||
|
||||
@@ -18,18 +21,60 @@ class PosAdyenController(http.Controller):
|
||||
return
|
||||
|
||||
_logger.info('notification received from adyen:\n%s', pprint.pformat(data))
|
||||
terminal_identifier = data['SaleToPOIResponse']['MessageHeader']['POIID']
|
||||
payment_method_sudo = request.env['pos.payment.method'].sudo().search([('adyen_terminal_identifier', '=', terminal_identifier)], limit=1)
|
||||
pos_session_id = int(data["SaleToPOIResponse"]["PaymentResponse"]["SaleData"]["SaleTransactionID"]["TransactionID"].split("-")[1])
|
||||
pos_session_sudo = request.env["pos.session"].sudo().browse(pos_session_id)
|
||||
|
||||
if payment_method_sudo:
|
||||
# These are only used to see if the terminal is reachable,
|
||||
# store the most recent ID we received.
|
||||
if data['SaleToPOIResponse'].get('DiagnosisResponse'):
|
||||
payment_method_sudo.adyen_latest_diagnosis = data['SaleToPOIResponse']['MessageHeader']['ServiceID']
|
||||
else:
|
||||
payment_method_sudo.adyen_latest_response = json.dumps(data)
|
||||
request.env["bus.bus"].sudo()._sendone(pos_session_sudo._get_bus_channel_name(), "ADYEN_LATEST_RESPONSE", pos_session_sudo.config_id.id)
|
||||
else:
|
||||
_logger.error('received a message for a terminal not registered in Odoo: %s', terminal_identifier)
|
||||
msg_header = data['SaleToPOIResponse'].get('MessageHeader')
|
||||
if not msg_header \
|
||||
or msg_header.get('ProtocolVersion') != '3.0' \
|
||||
or msg_header.get('MessageClass') != 'Service' \
|
||||
or msg_header.get('MessageType') != 'Response' \
|
||||
or msg_header.get('MessageCategory') != 'Payment' \
|
||||
or not msg_header.get('POIID'):
|
||||
_logger.warning('Received an unexpected Adyen notification')
|
||||
return
|
||||
|
||||
terminal_identifier = msg_header['POIID']
|
||||
adyen_pm_sudo = request.env['pos.payment.method'].sudo().search([('adyen_terminal_identifier', '=', terminal_identifier)], limit=1)
|
||||
if not adyen_pm_sudo:
|
||||
_logger.warning('Received an Adyen event notification for a terminal not registered in Odoo: %s', terminal_identifier)
|
||||
return
|
||||
|
||||
try:
|
||||
adyen_additional_response = data['SaleToPOIResponse']['PaymentResponse']['Response']['AdditionalResponse']
|
||||
pos_hmac = PosAdyenController._get_additional_data_from_unparsed(adyen_additional_response, 'metadata.pos_hmac')
|
||||
|
||||
if not pos_hmac or not consteq(pos_hmac, adyen_pm_sudo._get_hmac(msg_header['SaleID'], msg_header['ServiceID'], msg_header['POIID'], data['SaleToPOIResponse']['PaymentResponse']['SaleData']['SaleTransactionID']['TransactionID'])):
|
||||
_logger.warning('Received an invalid Adyen event notification (invalid hmac): \n%s', pprint.pformat(data))
|
||||
return
|
||||
|
||||
# The HMAC is removed to prevent anyone from using it in place of Adyen.
|
||||
pos_hmac_metadata_raw = 'metadata.pos_hmac='+pos_hmac
|
||||
safe_additional_response = adyen_additional_response.replace('&'+pos_hmac_metadata_raw, '').replace(pos_hmac_metadata_raw, '')
|
||||
data['SaleToPOIResponse']['PaymentResponse']['Response']['AdditionalResponse'] = safe_additional_response
|
||||
except (KeyError, AttributeError):
|
||||
_logger.warning('Received an invalid Adyen event notification: \n%s', pprint.pformat(data))
|
||||
return
|
||||
|
||||
return self._process_payment_response(data, adyen_pm_sudo)
|
||||
|
||||
@staticmethod
|
||||
def _get_additional_data_from_unparsed(adyen_additional_response, data_key):
|
||||
parsed_adyen_additional_response = parse_qs(adyen_additional_response)
|
||||
return PosAdyenController._get_additional_data_from_parsed(parsed_adyen_additional_response, data_key)
|
||||
|
||||
@staticmethod
|
||||
def _get_additional_data_from_parsed(parsed_adyen_additional_response, data_key):
|
||||
data_value = parsed_adyen_additional_response.get(data_key)
|
||||
return data_value[0] if data_value and len(data_value) == 1 else None
|
||||
|
||||
def _process_payment_response(self, data, adyen_pm_sudo):
|
||||
transaction_id = data['SaleToPOIResponse']['PaymentResponse']['SaleData']['SaleTransactionID']['TransactionID']
|
||||
if not transaction_id:
|
||||
return
|
||||
transaction_id_parts = transaction_id.split("--")
|
||||
if len(transaction_id_parts) != 2:
|
||||
return
|
||||
pos_session_id = int(transaction_id_parts[1])
|
||||
pos_session_sudo = request.env["pos.session"].sudo().browse(pos_session_id)
|
||||
adyen_pm_sudo.adyen_latest_response = json.dumps(data)
|
||||
request.env['bus.bus'].sudo()._sendone(pos_session_sudo._get_bus_channel_name(), 'ADYEN_LATEST_RESPONSE', pos_session_sudo.config_id.id)
|
||||
return request.make_json_response('[accepted]') # https://docs.adyen.com/point-of-sale/design-your-integration/choose-your-architecture/cloud/#guarantee
|
||||
|
||||
@@ -6,13 +6,17 @@ import pprint
|
||||
import random
|
||||
import requests
|
||||
import string
|
||||
from urllib.parse import parse_qs
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from odoo import fields, models, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.exceptions import ValidationError, UserError, AccessDenied
|
||||
from odoo.tools import hmac
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
UNPREDICTABLE_ADYEN_DATA = object() # sentinel
|
||||
|
||||
class PosPaymentMethod(models.Model):
|
||||
_inherit = 'pos.payment.method'
|
||||
|
||||
@@ -20,9 +24,9 @@ class PosPaymentMethod(models.Model):
|
||||
return super(PosPaymentMethod, self)._get_payment_terminal_selection() + [('adyen', 'Adyen')]
|
||||
|
||||
# Adyen
|
||||
adyen_api_key = fields.Char(string="Adyen API key", help='Used when connecting to Adyen: https://docs.adyen.com/user-management/how-to-get-the-api-key/#description', copy=False)
|
||||
adyen_api_key = fields.Char(string="Adyen API key", help='Used when connecting to Adyen: https://docs.adyen.com/user-management/how-to-get-the-api-key/#description', copy=False, groups='base.group_erp_manager')
|
||||
adyen_terminal_identifier = fields.Char(help='[Terminal model]-[Serial number], for example: P400Plus-123456789', copy=False)
|
||||
adyen_test_mode = fields.Boolean(help='Run transactions in the test environment.')
|
||||
adyen_test_mode = fields.Boolean(help='Run transactions in the test environment.', groups='base.group_erp_manager')
|
||||
|
||||
adyen_latest_response = fields.Char(copy=False, groups='base.group_erp_manager') # used to buffer the latest asynchronous notification from Adyen.
|
||||
adyen_latest_diagnosis = fields.Char(copy=False, groups='base.group_erp_manager') # used to determine if the terminal is still connected.
|
||||
@@ -52,54 +56,179 @@ class PosPaymentMethod(models.Model):
|
||||
}
|
||||
|
||||
def _is_write_forbidden(self, fields):
|
||||
whitelisted_fields = {'adyen_latest_response', 'adyen_latest_diagnosis'}
|
||||
return super(PosPaymentMethod, self)._is_write_forbidden(fields - whitelisted_fields)
|
||||
|
||||
def _adyen_diagnosis_request_data(self, pos_config_name):
|
||||
service_id = ''.join(random.choices(string.ascii_letters + string.digits, k=10))
|
||||
return {
|
||||
"SaleToPOIRequest": {
|
||||
"MessageHeader": {
|
||||
"ProtocolVersion": "3.0",
|
||||
"MessageClass": "Service",
|
||||
"MessageCategory": "Diagnosis",
|
||||
"MessageType": "Request",
|
||||
"ServiceID": service_id,
|
||||
"SaleID": pos_config_name,
|
||||
"POIID": self.adyen_terminal_identifier,
|
||||
},
|
||||
"DiagnosisRequest": {
|
||||
"HostDiagnosisFlag": False
|
||||
}
|
||||
}
|
||||
}
|
||||
return super(PosPaymentMethod, self)._is_write_forbidden(fields - {'adyen_latest_response'})
|
||||
|
||||
def get_latest_adyen_status(self):
|
||||
self.ensure_one()
|
||||
if not self.env.su and not self.user_has_groups('point_of_sale.group_pos_user'):
|
||||
raise AccessDenied()
|
||||
|
||||
latest_response = self.sudo().adyen_latest_response
|
||||
latest_response = json.loads(latest_response) if latest_response else False
|
||||
return latest_response
|
||||
|
||||
def proxy_adyen_request(self, data, operation=False):
|
||||
''' Necessary because Adyen's endpoints don't have CORS enabled '''
|
||||
if data.get('SaleToPOIRequest') and data['SaleToPOIRequest']['MessageHeader']['MessageCategory'] == 'Payment': # Clear only if it is a payment request
|
||||
self.ensure_one()
|
||||
if not self.env.su and not self.user_has_groups('point_of_sale.group_pos_user'):
|
||||
raise AccessDenied()
|
||||
if not data:
|
||||
raise UserError(_('Invalid Adyen request'))
|
||||
|
||||
if 'SaleToPOIRequest' in data and data['SaleToPOIRequest']['MessageHeader']['MessageCategory'] == 'Payment': # Clear only if it is a payment request
|
||||
self.sudo().adyen_latest_response = '' # avoid handling old responses multiple times
|
||||
|
||||
if not operation:
|
||||
operation = 'terminal_request'
|
||||
|
||||
# These checks are not optimal. This RPC method should be changed.
|
||||
|
||||
is_capture_data = operation == 'capture' and hasattr(self, 'adyen_merchant_account') and self._is_valid_adyen_request_data(data, {
|
||||
'originalReference': UNPREDICTABLE_ADYEN_DATA,
|
||||
'modificationAmount': {
|
||||
'value': UNPREDICTABLE_ADYEN_DATA,
|
||||
'currency': UNPREDICTABLE_ADYEN_DATA,
|
||||
},
|
||||
'merchantAccount': self.adyen_merchant_account,
|
||||
})
|
||||
|
||||
is_adjust_data = operation == 'adjust' and hasattr(self, 'adyen_merchant_account') and self._is_valid_adyen_request_data(data, {
|
||||
'originalReference': UNPREDICTABLE_ADYEN_DATA,
|
||||
'modificationAmount': {
|
||||
'value': UNPREDICTABLE_ADYEN_DATA,
|
||||
'currency': UNPREDICTABLE_ADYEN_DATA,
|
||||
},
|
||||
'merchantAccount': self.adyen_merchant_account,
|
||||
'additionalData': {
|
||||
'industryUsage': 'DelayedCharge',
|
||||
},
|
||||
})
|
||||
|
||||
is_cancel_data = operation == 'terminal_request' and self._is_valid_adyen_request_data(data, {
|
||||
'SaleToPOIRequest': {
|
||||
'MessageHeader': self._get_expected_message_header('Abort'),
|
||||
'AbortRequest': {
|
||||
'AbortReason': 'MerchantAbort',
|
||||
'MessageReference': {
|
||||
'MessageCategory': 'Payment',
|
||||
'SaleID': UNPREDICTABLE_ADYEN_DATA,
|
||||
'ServiceID': UNPREDICTABLE_ADYEN_DATA,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
is_payment_request_with_acquirer_data = operation == 'terminal_request' and self._is_valid_adyen_request_data(data, self._get_expected_payment_request(True))
|
||||
|
||||
if is_payment_request_with_acquirer_data:
|
||||
parsed_sale_to_acquirer_data = parse_qs(data['SaleToPOIRequest']['PaymentRequest']['SaleData']['SaleToAcquirerData'])
|
||||
valid_acquirer_data = self._get_valid_acquirer_data()
|
||||
is_payment_request_with_acquirer_data = len(parsed_sale_to_acquirer_data.keys()) <= len(valid_acquirer_data.keys())
|
||||
if is_payment_request_with_acquirer_data:
|
||||
for key, values in parsed_sale_to_acquirer_data.items():
|
||||
if len(values) != 1:
|
||||
is_payment_request_with_acquirer_data = False
|
||||
break
|
||||
value = values[0]
|
||||
valid_value = valid_acquirer_data.get(key)
|
||||
if valid_value == UNPREDICTABLE_ADYEN_DATA:
|
||||
continue
|
||||
if value != valid_value:
|
||||
is_payment_request_with_acquirer_data = False
|
||||
break
|
||||
|
||||
is_payment_request_without_acquirer_data = operation == 'terminal_request' and self._is_valid_adyen_request_data(data, self._get_expected_payment_request(False))
|
||||
|
||||
if not is_payment_request_without_acquirer_data and not is_payment_request_with_acquirer_data and not is_adjust_data and not is_cancel_data and not is_capture_data:
|
||||
raise UserError(_('Invalid Adyen request'))
|
||||
|
||||
if is_payment_request_with_acquirer_data or is_payment_request_without_acquirer_data:
|
||||
acquirer_data = data['SaleToPOIRequest']['PaymentRequest']['SaleData'].get('SaleToAcquirerData')
|
||||
msg_header = data['SaleToPOIRequest']['MessageHeader']
|
||||
metadata = 'metadata.pos_hmac=' + self._get_hmac(msg_header['SaleID'], msg_header['ServiceID'], msg_header['POIID'], data['SaleToPOIRequest']['PaymentRequest']['SaleData']['SaleTransactionID']['TransactionID'])
|
||||
|
||||
data['SaleToPOIRequest']['PaymentRequest']['SaleData']['SaleToAcquirerData'] = acquirer_data + '&' + metadata if acquirer_data else metadata
|
||||
|
||||
return self._proxy_adyen_request_direct(data, operation)
|
||||
|
||||
@api.model
|
||||
def _is_valid_adyen_request_data(self, provided_data, expected_data):
|
||||
if not isinstance(provided_data, dict) or set(provided_data.keys()) != set(expected_data.keys()):
|
||||
return False
|
||||
|
||||
for provided_key, provided_value in provided_data.items():
|
||||
expected_value = expected_data[provided_key]
|
||||
if expected_value == UNPREDICTABLE_ADYEN_DATA:
|
||||
continue
|
||||
if isinstance(expected_value, dict):
|
||||
if not self._is_valid_adyen_request_data(provided_value, expected_value):
|
||||
return False
|
||||
else:
|
||||
if provided_value != expected_value:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _get_expected_message_header(self, expected_message_category):
|
||||
return {
|
||||
'ProtocolVersion': '3.0',
|
||||
'MessageClass': 'Service',
|
||||
'MessageType': 'Request',
|
||||
'MessageCategory': expected_message_category,
|
||||
'SaleID': UNPREDICTABLE_ADYEN_DATA,
|
||||
'ServiceID': UNPREDICTABLE_ADYEN_DATA,
|
||||
'POIID': self.adyen_terminal_identifier,
|
||||
}
|
||||
|
||||
def _get_expected_payment_request(self, with_acquirer_data):
|
||||
res = {
|
||||
'SaleToPOIRequest': {
|
||||
'MessageHeader': self._get_expected_message_header('Payment'),
|
||||
'PaymentRequest': {
|
||||
'SaleData': {
|
||||
'SaleTransactionID': {
|
||||
'TransactionID': UNPREDICTABLE_ADYEN_DATA,
|
||||
'TimeStamp': UNPREDICTABLE_ADYEN_DATA,
|
||||
},
|
||||
},
|
||||
'PaymentTransaction': {
|
||||
'AmountsReq': {
|
||||
'Currency': UNPREDICTABLE_ADYEN_DATA,
|
||||
'RequestedAmount': UNPREDICTABLE_ADYEN_DATA,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if with_acquirer_data:
|
||||
res['SaleToPOIRequest']['PaymentRequest']['SaleData']['SaleToAcquirerData'] = UNPREDICTABLE_ADYEN_DATA
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _get_valid_acquirer_data(self):
|
||||
return {
|
||||
'tenderOption': 'AskGratuity',
|
||||
'authorisationType': 'PreAuth'
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _get_hmac(self, sale_id, service_id, poi_id, sale_transaction_id):
|
||||
return hmac(
|
||||
env=self.env(su=True),
|
||||
scope='pos_adyen_payment',
|
||||
message=(sale_id, service_id, poi_id, sale_transaction_id)
|
||||
)
|
||||
|
||||
def _proxy_adyen_request_direct(self, data, operation):
|
||||
self.ensure_one()
|
||||
TIMEOUT = 10
|
||||
|
||||
_logger.info('request to adyen\n%s', pprint.pformat(data))
|
||||
_logger.info('Request to Adyen by user #%d:\n%s', self.env.uid, pprint.pformat(data))
|
||||
|
||||
environment = 'test' if self.adyen_test_mode else 'live'
|
||||
environment = 'test' if self.sudo().adyen_test_mode else 'live'
|
||||
endpoint = self._get_adyen_endpoints()[operation] % environment
|
||||
headers = {
|
||||
'x-api-key': self.adyen_api_key,
|
||||
'x-api-key': self.sudo().adyen_api_key,
|
||||
}
|
||||
req = requests.post(endpoint, json=data, headers=headers, timeout=TIMEOUT)
|
||||
|
||||
|
||||
@@ -264,7 +264,7 @@ export class PaymentAdyen extends PaymentInterface {
|
||||
const payment_result = payment_response.PaymentResult;
|
||||
|
||||
const cashier_receipt = payment_response.PaymentReceipt.find((receipt) => {
|
||||
receipt.DocumentQualifier == "CashierReceipt";
|
||||
return receipt.DocumentQualifier == "CashierReceipt";
|
||||
});
|
||||
|
||||
if (cashier_receipt) {
|
||||
@@ -274,7 +274,7 @@ export class PaymentAdyen extends PaymentInterface {
|
||||
}
|
||||
|
||||
const customer_receipt = payment_response.PaymentReceipt.find((receipt) => {
|
||||
receipt.DocumentQualifier == "CustomerReceipt";
|
||||
return receipt.DocumentQualifier == "CustomerReceipt";
|
||||
});
|
||||
|
||||
if (customer_receipt) {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='use_payment_terminal']" position="after">
|
||||
<!-- Adyen -->
|
||||
<field name="adyen_api_key" invisible="use_payment_terminal != 'adyen'" required="use_payment_terminal == 'adyen'"/>
|
||||
<field name="adyen_api_key" invisible="use_payment_terminal != 'adyen'" required="use_payment_terminal == 'adyen'" password="True"/>
|
||||
<field name="adyen_terminal_identifier" invisible="use_payment_terminal != 'adyen'" required="use_payment_terminal == 'adyen'"/>
|
||||
<field name="adyen_test_mode" invisible="use_payment_terminal != 'adyen'" required="use_payment_terminal == 'adyen'"/>
|
||||
</xpath>
|
||||
|
||||
@@ -193,7 +193,7 @@ class PosSelfOrderController(http.Controller):
|
||||
if not order_sudo or not payment_method_sudo or payment_method_sudo not in order_sudo.config_id.payment_method_ids:
|
||||
raise NotFound("Order or payment method not found")
|
||||
|
||||
status = payment_method_sudo.payment_request_from_kiosk(order_sudo)
|
||||
status = payment_method_sudo._payment_request_from_kiosk(order_sudo)
|
||||
|
||||
if not status:
|
||||
raise BadRequest("Something went wrong")
|
||||
|
||||
@@ -5,5 +5,5 @@ class PosPaymentMethod(models.Model):
|
||||
_inherit = "pos.payment.method"
|
||||
|
||||
# will be overridden.
|
||||
def payment_request_from_kiosk(self, order):
|
||||
def _payment_request_from_kiosk(self, order):
|
||||
pass
|
||||
|
||||
@@ -1,43 +1,53 @@
|
||||
import json
|
||||
import logging
|
||||
from odoo.addons.pos_adyen.controllers.main import PosAdyenController
|
||||
from odoo import http, fields
|
||||
from odoo import fields
|
||||
from odoo.http import request
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PosSelfAdyenController(PosAdyenController):
|
||||
@http.route()
|
||||
def notification(self):
|
||||
super().notification()
|
||||
|
||||
data = json.loads(request.httprequest.data)
|
||||
if data.get('SaleToPOIResponse'):
|
||||
order_reference = data['SaleToPOIResponse']['PaymentResponse']['SaleData']['SaleTransactionID']['TransactionID']
|
||||
payment_result = data['SaleToPOIResponse']['PaymentResponse']['Response']['Result']
|
||||
order_sudo = request.env['pos.order'].sudo().search([('pos_reference', '=', order_reference)], limit=1)
|
||||
order = order_sudo.sudo(False).with_user(order_sudo.session_id.config_id.self_ordering_default_user_id).with_company(order_sudo.session_id.config_id.company_id)
|
||||
terminal_identifier = data['SaleToPOIResponse']['MessageHeader']['POIID']
|
||||
payment_method = request.env['pos.payment.method'].sudo().search([('adyen_terminal_identifier', '=', terminal_identifier)], limit=1)
|
||||
def _process_payment_response(self, data, adyen_pm_sudo):
|
||||
self_order_id = None
|
||||
try:
|
||||
self_order_id = PosAdyenController._get_additional_data_from_unparsed(data['SaleToPOIResponse']['PaymentResponse']['Response']['AdditionalResponse'], 'metadata.self_order_id')
|
||||
except KeyError:
|
||||
self_order_id = None
|
||||
|
||||
if payment_result == 'Success' and order.config_id.self_ordering_mode == 'kiosk':
|
||||
payment_amount = data['SaleToPOIResponse']['PaymentResponse']['PaymentResult']['AmountsResp']['AuthorizedAmount']
|
||||
card_type = data['SaleToPOIResponse']['PaymentResponse']['PaymentResult']['PaymentInstrumentData']['CardData']['PaymentBrand']
|
||||
transaction_id = data['SaleToPOIResponse']['PaymentResponse']['SaleData']['SaleTransactionID']['TransactionID']
|
||||
order.add_payment({
|
||||
'amount': payment_amount,
|
||||
'payment_date': fields.Datetime.now(),
|
||||
'payment_method_id': payment_method.id,
|
||||
'card_type': card_type,
|
||||
'cardholder_name': '',
|
||||
'transaction_id': transaction_id,
|
||||
'payment_status': payment_result,
|
||||
'ticket': '',
|
||||
'pos_order_id': order.id
|
||||
})
|
||||
order.action_pos_order_paid()
|
||||
order._send_order()
|
||||
if not self_order_id:
|
||||
return super()._process_payment_response(data, adyen_pm_sudo)
|
||||
|
||||
if order.config_id.self_ordering_mode == 'kiosk':
|
||||
order.env['bus.bus']._sendone(f'pos_config-{order.config_id.access_token}', 'PAYMENT_STATUS', {
|
||||
'payment_result': payment_result,
|
||||
'order': order._export_for_self_order(),
|
||||
})
|
||||
order_sudo = request.env['pos.order'].sudo().search([('id', '=', self_order_id)], limit=1)
|
||||
if not order_sudo:
|
||||
_logger.warning('Received an Adyen event notification for the self order #%d that does not exist (anymore)', self_order_id)
|
||||
return request.make_json_response('[accepted]') # https://docs.adyen.com/point-of-sale/design-your-integration/choose-your-architecture/cloud/#guarantee
|
||||
|
||||
order = order_sudo.sudo(False).with_user(order_sudo.session_id.config_id.self_ordering_default_user_id).with_company(order_sudo.session_id.config_id.company_id)
|
||||
|
||||
payment_result = data['SaleToPOIResponse']['PaymentResponse']['Response']['Result']
|
||||
|
||||
if payment_result == 'Success' and order.config_id.self_ordering_mode == 'kiosk':
|
||||
payment_amount = data['SaleToPOIResponse']['PaymentResponse']['PaymentResult']['AmountsResp']['AuthorizedAmount']
|
||||
card_type = data['SaleToPOIResponse']['PaymentResponse']['PaymentResult']['PaymentInstrumentData']['CardData']['PaymentBrand']
|
||||
transaction_id = data['SaleToPOIResponse']['PaymentResponse']['SaleData']['SaleTransactionID']['TransactionID']
|
||||
order.add_payment({
|
||||
'amount': payment_amount,
|
||||
'payment_date': fields.Datetime.now(),
|
||||
'payment_method_id': adyen_pm_sudo.id,
|
||||
'card_type': card_type,
|
||||
'cardholder_name': '',
|
||||
'transaction_id': transaction_id,
|
||||
'payment_status': payment_result,
|
||||
'ticket': '',
|
||||
'pos_order_id': order.id
|
||||
})
|
||||
order.action_pos_order_paid()
|
||||
order._send_order()
|
||||
|
||||
if order.config_id.self_ordering_mode == 'kiosk':
|
||||
order.env['bus.bus']._sendone(f'pos_config-{order.config_id.access_token}', 'PAYMENT_STATUS', {
|
||||
'payment_result': payment_result,
|
||||
'order': order._export_for_self_order(),
|
||||
})
|
||||
return request.make_json_response('[accepted]') # https://docs.adyen.com/point-of-sale/design-your-integration/choose-your-architecture/cloud/#guarantee
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
from datetime import datetime, timezone
|
||||
import random
|
||||
from odoo import models
|
||||
from odoo import models, api
|
||||
from odoo.addons.pos_adyen.models.pos_payment_method import UNPREDICTABLE_ADYEN_DATA
|
||||
|
||||
|
||||
class PosPaymentMethod(models.Model):
|
||||
_inherit = "pos.payment.method"
|
||||
|
||||
def payment_request_from_kiosk(self, order):
|
||||
@api.model
|
||||
def _get_valid_acquirer_data(self):
|
||||
res = super()._get_valid_acquirer_data()
|
||||
res['metadata.self_order_id'] = UNPREDICTABLE_ADYEN_DATA
|
||||
return res
|
||||
|
||||
def _payment_request_from_kiosk(self, order):
|
||||
if self.use_payment_terminal != 'adyen':
|
||||
return super().payment_request_from_kiosk(order)
|
||||
return super()._payment_request_from_kiosk(order)
|
||||
else:
|
||||
pos_config = order.session_id.config_id
|
||||
random_number = random.randrange(10**9, 10**10 - 1)
|
||||
@@ -31,6 +38,7 @@ class PosPaymentMethod(models.Model):
|
||||
'TransactionID': order.pos_reference, # your reference to identify a payment.
|
||||
'TimeStamp': datetime.now(tz=timezone.utc).isoformat(timespec='seconds'), # date and time of the request in UTC format.
|
||||
},
|
||||
'SaleToAcquirerData': 'metadata.self_order_id=' + str(order.id),
|
||||
},
|
||||
'PaymentTransaction': {
|
||||
'AmountsReq': {
|
||||
@@ -44,7 +52,4 @@ class PosPaymentMethod(models.Model):
|
||||
|
||||
req = self.proxy_adyen_request(data)
|
||||
|
||||
if not req and req.get('error'):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return req and (isinstance(req, bool) or not req.get('error'))
|
||||
|
||||
@@ -4,8 +4,8 @@ from odoo import models
|
||||
class PosPaymentMethod(models.Model):
|
||||
_inherit = "pos.payment.method"
|
||||
|
||||
def payment_request_from_kiosk(self, order):
|
||||
def _payment_request_from_kiosk(self, order):
|
||||
if self.use_payment_terminal != 'stripe':
|
||||
return super().payment_request_from_kiosk(order)
|
||||
return super()._payment_request_from_kiosk(order)
|
||||
else:
|
||||
return self.stripe_payment_intent(order.amount_total)
|
||||
|
||||
Reference in New Issue
Block a user