[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/',
'depends': [
'delivery',
'hibou_professional',
'stock',
'website_sale',
'website_sale_delivery',
'website_payment',
],
'data': [

View File

@@ -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,

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

View File

@@ -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'
},
]
}

View File

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