[IMP][H4547] website_sale_signifyd: API v3 + cleanup WIP

This commit is contained in:
Milan
2024-10-21 20:07:42 +02:00
parent dc248d1c20
commit 78ae6d9751
6 changed files with 78 additions and 57 deletions

View File

@@ -8,10 +8,9 @@ Automate Order Fraud Detection with the Signifyd API.
""", """,
'website': 'https://hibou.io/', 'website': 'https://hibou.io/',
'depends': [ 'depends': [
'delivery',
'hibou_professional', 'hibou_professional',
'stock', 'stock',
'website_sale', 'website_sale_delivery',
'website_payment', 'website_payment',
], ],
'data': [ 'data': [

View File

@@ -43,20 +43,23 @@ class SaleOrder(models.Model):
return True return True
def post_signifyd_case(self): 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 return
browser_ip_address = request.httprequest.environ['REMOTE_ADDR'] browser_ip_address = request.httprequest.environ['REMOTE_ADDR']
if request.session: if request.session:
checkout_token = request.session.session_token checkout_token = request.session.session_token
order_session_id = checkout_token order_session_id = checkout_token
else: else:
checkout_token = '' checkout_token = ''
# Session values for Signifyd post # Session values for Signifyd post
sig_vals = self._prepare_signifyd_case_values(order_session_id, checkout_token, browser_ip_address) 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) response = signifyd_api.post_case(sig_vals)
success_response = response.get('signifydId')
success_response = case.get('investigationId')
if success_response: if success_response:
new_case = self.env['signifyd.case'].create({ new_case = self.env['signifyd.case'].create({
'order_id': self.id, 'order_id': self.id,
@@ -77,7 +80,7 @@ class SaleOrder(models.Model):
if coverage_all in acquirer_coverage_types: if coverage_all in acquirer_coverage_types:
return coverage_all return coverage_all
# 'NONE' if specified by all acquirers # '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 return coverage_none
# Specific acquirer-level coverage types # Specific acquirer-level coverage types
if acquirer_coverage_types - coverage_none: if acquirer_coverage_types - coverage_none:
@@ -116,7 +119,7 @@ class SaleOrder(models.Model):
# FIXME: UUID? # FIXME: UUID?
'orderId': self.id, 'orderId': self.id,
'purchase': { 'purchase': {
'createdAt': self.date_order.isoformat(timespec='seconds'), 'createdAt': self.date_order.isoformat(timespec='seconds') + '+00:00',
'orderChannel': 'WEB', 'orderChannel': 'WEB',
'totalPrice': self.amount_total, 'totalPrice': self.amount_total,
'totalShippingCost': self.amount_delivery, 'totalShippingCost': self.amount_delivery,
@@ -143,8 +146,8 @@ class SaleOrder(models.Model):
'fulfillmentMethod': carrier.signifyd_fulfillment_method, 'fulfillmentMethod': carrier.signifyd_fulfillment_method,
} for carrier in self.carrier_id } for carrier in self.carrier_id
], ],
'coverageRequests': coverage_codes,
}, },
'coverageRequests': coverage_codes,
'transactions': [ 'transactions': [
{ {
'parentTransactionId': None, 'parentTransactionId': None,

View File

@@ -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()

View File

@@ -78,18 +78,14 @@ class SignifydCase(models.Model):
subtype_xmlid='website_sale_signifyd.disposition_change') subtype_xmlid='website_sale_signifyd.disposition_change')
return res return res
@api.model # @api.model
def post_case(self, connector, values): # def post_case(self, connector, values):
headers = connector.get_headers() # headers = connector.get_headers()
data = json.dumps(values, indent=4, sort_keys=True, default=str) # # 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` # # TODO this should be in `signifyd.connector`
r = requests.post( # r = requests.post(url=url, headers=headers, json=values)
connector.API_URL + '/cases', # return r.json()
headers=headers,
data=data,
)
return r.json()
def get_case(self): def get_case(self):
self.ensure_one() self.ensure_one()

View File

@@ -6,6 +6,7 @@ from base64 import b64encode
import json import json
from odoo import api, fields, models from odoo import api, fields, models
from .signifyd_api import SignifydAPI
class SignifydConnector(models.Model): class SignifydConnector(models.Model):
@@ -20,40 +21,26 @@ class SignifydConnector(models.Model):
notify_user_ids = fields.Many2many('res.users', string='Receive decline notifications') notify_user_ids = fields.Many2many('res.users', string='Receive decline notifications')
website_ids = fields.One2many('website', 'signifyd_connector_id', string='Used on Websites') website_ids = fields.One2many('website', 'signifyd_connector_id', string='Used on Websites')
# TODO: remove options no longer available in api v3 # TODO: remove options no longer available in api v3
signifyd_case_type = fields.Selection([ # signifyd_case_type = fields.Selection([
('', 'No Case'), # ('', 'No Case'),
('SCORE', 'Score'), # ('SCORE', 'Score'),
('DECISION', 'Decision'), # ('DECISION', 'Decision'),
('GUARANTEE', 'Guarantee'), # ('GUARANTEE', 'Guarantee'),
], string='Default Case Creation', help='Used for internal/admin orders, overridden by payment acquirer.', # ], string='Default Case Creation', help='Used for internal/admin orders, overridden by payment acquirer.',
required=True, default='') # required=True, default='')
signifyd_coverage_ids = fields.Many2many('signifyd.coverage', string='Available Coverage Types', signifyd_coverage_ids = fields.Many2many('signifyd.coverage', string='Available Coverage Types',
help='Note that exclusive coverage types will only allow one to be selected.') 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') @api.onchange('signifyd_coverage_ids')
def _onchange_signifyd_coverage_ids(self): def _onchange_signifyd_coverage_ids(self):
self.signifyd_coverage_ids = self.signifyd_coverage_ids._apply_exclusivity() 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): def get_connection(self):
self.ensure_one() if not self:
# Check for prod or test mode return
if self.test_mode: return SignifydAPI(self.name, self.secret_key, self.teamid)
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 register_webhooks(self): def register_webhooks(self):
self.ensure_one() self.ensure_one()
@@ -69,27 +56,27 @@ class SignifydConnector(models.Model):
if not base_url: if not base_url:
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
values = { values = {
"webhooks": [ 'webhooks': [
# Given we are creating the cases, we do not need to know about it # Given we are creating the cases, we do not need to know about it
# { # {
# "event": "CASE_CREATION", # "event": "CASE_CREATION",
# "url": base_url + "/signifyd/cases/update" # "url": base_url + "/signifyd/cases/update"
# }, # },
{ {
"event": "CASE_RESCORE", 'event': 'CASE_RESCORE',
"url": base_url + "/signifyd/cases/update" 'url': base_url + '/signifyd/cases/update'
}, },
{ {
"event": "CASE_REVIEW", 'event': 'CASE_REVIEW',
"url": base_url + "/signifyd/cases/update" 'url': base_url + '/signifyd/cases/update'
}, },
{ {
"event": "GUARANTEE_COMPLETION", 'event': 'GUARANTEE_COMPLETION',
"url": base_url + "/signifyd/cases/update" 'url': base_url + '/signifyd/cases/update'
}, },
{ {
"event": "DECISION_MADE", 'event': 'DECISION_MADE',
"url": base_url + "/signifyd/cases/update" 'url': base_url + '/signifyd/cases/update'
}, },
] ]
} }

View File

@@ -107,7 +107,7 @@
<field name="secret_key" attrs="{'invisible': [('test_mode', '=', True)]}"/> <field name="secret_key" attrs="{'invisible': [('test_mode', '=', True)]}"/>
<field name="secret_key_test" attrs="{'invisible': [('test_mode', '!=', True)]}"/> <field name="secret_key_test" attrs="{'invisible': [('test_mode', '!=', True)]}"/>
<!-- <field name="signifyd_case_type" /> --> <!-- <field name="signifyd_case_type" /> -->
<field name="signifyd_coverage_ids"/> <field name="signifyd_coverage_ids" widget="many2many_tags"/>
<p class="text-muted"> <p class="text-muted">
Optional: Add users to be notified if a sale order is declined by Signifyd. Optional: Add users to be notified if a sale order is declined by Signifyd.
</p> </p>