From 78ae6d9751f1c89b8eb6e42118cfe7b0f3a697ab Mon Sep 17 00:00:00 2001 From: Milan Date: Mon, 21 Oct 2024 20:07:42 +0200 Subject: [PATCH] [IMP][H4547] website_sale_signifyd: API v3 + cleanup WIP --- website_sale_signifyd/__manifest__.py | 3 +- website_sale_signifyd/models/sale_order.py | 17 +++--- website_sale_signifyd/models/signifyd_api.py | 36 ++++++++++++ website_sale_signifyd/models/signifyd_case.py | 20 +++---- .../models/signifyd_connector.py | 57 +++++++------------ .../views/signifyd_views.xml | 2 +- 6 files changed, 78 insertions(+), 57 deletions(-) create mode 100644 website_sale_signifyd/models/signifyd_api.py diff --git a/website_sale_signifyd/__manifest__.py b/website_sale_signifyd/__manifest__.py index 56702d3c..801c350f 100644 --- a/website_sale_signifyd/__manifest__.py +++ b/website_sale_signifyd/__manifest__.py @@ -8,10 +8,9 @@ Automate Order Fraud Detection with the Signifyd API. """, 'website': 'https://hibou.io/', 'depends': [ - 'delivery', 'hibou_professional', 'stock', - 'website_sale', + 'website_sale_delivery', 'website_payment', ], 'data': [ diff --git a/website_sale_signifyd/models/sale_order.py b/website_sale_signifyd/models/sale_order.py index 5832e943..66ae5331 100644 --- a/website_sale_signifyd/models/sale_order.py +++ b/website_sale_signifyd/models/sale_order.py @@ -43,20 +43,23 @@ class SaleOrder(models.Model): return True def post_signifyd_case(self): - if not self.website_id.signifyd_connector_id: + signifyd_api = self.website_id.signifyd_connector_id.get_connection() + if not signifyd_api: return + + browser_ip_address = request.httprequest.environ['REMOTE_ADDR'] if request.session: checkout_token = request.session.session_token order_session_id = checkout_token else: checkout_token = '' + # Session values for Signifyd post sig_vals = self._prepare_signifyd_case_values(order_session_id, checkout_token, browser_ip_address) - case = self.env['signifyd.case'].post_case(self.website_id.signifyd_connector_id, sig_vals) - - success_response = case.get('investigationId') + response = signifyd_api.post_case(sig_vals) + success_response = response.get('signifydId') if success_response: new_case = self.env['signifyd.case'].create({ 'order_id': self.id, @@ -77,7 +80,7 @@ class SaleOrder(models.Model): if coverage_all in acquirer_coverage_types: return coverage_all # 'NONE' if specified by all acquirers - if all(self.transaction_ids.acquirer_id.mapped(lambda a: a.signifyd_coverage_ids) == coverage_none): + if all(self.transaction_ids.acquirer_id.mapped(lambda a: a.signifyd_coverage_ids == coverage_none)): return coverage_none # Specific acquirer-level coverage types if acquirer_coverage_types - coverage_none: @@ -116,7 +119,7 @@ class SaleOrder(models.Model): # FIXME: UUID? 'orderId': self.id, 'purchase': { - 'createdAt': self.date_order.isoformat(timespec='seconds'), + 'createdAt': self.date_order.isoformat(timespec='seconds') + '+00:00', 'orderChannel': 'WEB', 'totalPrice': self.amount_total, 'totalShippingCost': self.amount_delivery, @@ -143,8 +146,8 @@ class SaleOrder(models.Model): 'fulfillmentMethod': carrier.signifyd_fulfillment_method, } for carrier in self.carrier_id ], - 'coverageRequests': coverage_codes, }, + 'coverageRequests': coverage_codes, 'transactions': [ { 'parentTransactionId': None, diff --git a/website_sale_signifyd/models/signifyd_api.py b/website_sale_signifyd/models/signifyd_api.py new file mode 100644 index 00000000..ee21e773 --- /dev/null +++ b/website_sale_signifyd/models/signifyd_api.py @@ -0,0 +1,36 @@ +from base64 import b64encode +import requests + +API_URL = 'https://api.signifyd.com/v3' + + +class SignifydAPI: + _teamid = None + _key = None + + def __init__(self, key, teamid): + self._key = b64encode(key.encode()).decode().strip('=') + self._teamid = teamid + + def _get_headers(self): + headers = { + 'Authorization': 'Basic ' + self._key, + 'Content-Type': 'application/json', + } + + def _request(self, method, path, headers=None, json=None): + headers = headers or {} + headers.update(self._get_headers()) + request = requests.request(method, API_URL + path, headers=headers, json=json) + return request + + def get(self, path, headers=None): + return self._request('GET', path, headers=headers) + + def post(self, path, headers=None, json=None): + return self._request('POST', path, headers=headers, json=json) + + def post_case(self, connector, values): + # data = json.dumps(values, indent=4, sort_keys=True, default=str) + r = self._post('/orders/events/sales', json=values) + return r.json() diff --git a/website_sale_signifyd/models/signifyd_case.py b/website_sale_signifyd/models/signifyd_case.py index d5b5133f..b3045061 100644 --- a/website_sale_signifyd/models/signifyd_case.py +++ b/website_sale_signifyd/models/signifyd_case.py @@ -78,18 +78,14 @@ class SignifydCase(models.Model): subtype_xmlid='website_sale_signifyd.disposition_change') return res - @api.model - def post_case(self, connector, values): - headers = connector.get_headers() - data = json.dumps(values, indent=4, sort_keys=True, default=str) - - # TODO this should be in `signifyd.connector` - r = requests.post( - connector.API_URL + '/cases', - headers=headers, - data=data, - ) - return r.json() + # @api.model + # def post_case(self, connector, values): + # headers = connector.get_headers() + # # data = json.dumps(values, indent=4, sort_keys=True, default=str) + # url = connector.API_URL + '/orders/events/sales' + # # TODO this should be in `signifyd.connector` + # r = requests.post(url=url, headers=headers, json=values) + # return r.json() def get_case(self): self.ensure_one() diff --git a/website_sale_signifyd/models/signifyd_connector.py b/website_sale_signifyd/models/signifyd_connector.py index 392a4e91..ffaa2610 100644 --- a/website_sale_signifyd/models/signifyd_connector.py +++ b/website_sale_signifyd/models/signifyd_connector.py @@ -6,6 +6,7 @@ from base64 import b64encode import json from odoo import api, fields, models +from .signifyd_api import SignifydAPI class SignifydConnector(models.Model): @@ -20,40 +21,26 @@ class SignifydConnector(models.Model): notify_user_ids = fields.Many2many('res.users', string='Receive decline notifications') website_ids = fields.One2many('website', 'signifyd_connector_id', string='Used on Websites') # TODO: remove options no longer available in api v3 - signifyd_case_type = fields.Selection([ - ('', 'No Case'), - ('SCORE', 'Score'), - ('DECISION', 'Decision'), - ('GUARANTEE', 'Guarantee'), - ], string='Default Case Creation', help='Used for internal/admin orders, overridden by payment acquirer.', - required=True, default='') + # signifyd_case_type = fields.Selection([ + # ('', 'No Case'), + # ('SCORE', 'Score'), + # ('DECISION', 'Decision'), + # ('GUARANTEE', 'Guarantee'), + # ], string='Default Case Creation', help='Used for internal/admin orders, overridden by payment acquirer.', + # required=True, default='') signifyd_coverage_ids = fields.Many2many('signifyd.coverage', string='Available Coverage Types', help='Note that exclusive coverage types will only allow one to be selected.') + teamid = fields.Char(string='Signifyd Team ID') @api.onchange('signifyd_coverage_ids') def _onchange_signifyd_coverage_ids(self): self.signifyd_coverage_ids = self.signifyd_coverage_ids._apply_exclusivity() - # TODO ideally this would be a regular constant - # however other entities currently use this by reference - API_URL = 'https://api.signifyd.com/v3' - def get_headers(self): - self.ensure_one() - # Check for prod or test mode - if self.test_mode: - api_key = self.secret_key_test - else: - api_key = self.secret_key - - b64_auth_key = b64encode(api_key.encode()).decode().replace('=', '') - - headers = { - 'Authorization': 'Basic ' + b64_auth_key, - 'Content-Type': 'application/json', - } - - return headers + def get_connection(self): + if not self: + return + return SignifydAPI(self.name, self.secret_key, self.teamid) def register_webhooks(self): self.ensure_one() @@ -69,27 +56,27 @@ class SignifydConnector(models.Model): if not base_url: base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') values = { - "webhooks": [ + 'webhooks': [ # Given we are creating the cases, we do not need to know about it # { # "event": "CASE_CREATION", # "url": base_url + "/signifyd/cases/update" # }, { - "event": "CASE_RESCORE", - "url": base_url + "/signifyd/cases/update" + 'event': 'CASE_RESCORE', + 'url': base_url + '/signifyd/cases/update' }, { - "event": "CASE_REVIEW", - "url": base_url + "/signifyd/cases/update" + 'event': 'CASE_REVIEW', + 'url': base_url + '/signifyd/cases/update' }, { - "event": "GUARANTEE_COMPLETION", - "url": base_url + "/signifyd/cases/update" + 'event': 'GUARANTEE_COMPLETION', + 'url': base_url + '/signifyd/cases/update' }, { - "event": "DECISION_MADE", - "url": base_url + "/signifyd/cases/update" + 'event': 'DECISION_MADE', + 'url': base_url + '/signifyd/cases/update' }, ] } diff --git a/website_sale_signifyd/views/signifyd_views.xml b/website_sale_signifyd/views/signifyd_views.xml index 1f6844cf..558fd1f8 100644 --- a/website_sale_signifyd/views/signifyd_views.xml +++ b/website_sale_signifyd/views/signifyd_views.xml @@ -107,7 +107,7 @@ - +

Optional: Add users to be notified if a sale order is declined by Signifyd.