[WIP] signifyd v3: working webhooks

This commit is contained in:
Milan
2024-12-16 16:19:05 +00:00
parent 35f29af2ac
commit 17c22ed9db
3 changed files with 36 additions and 85 deletions

View File

@@ -13,16 +13,20 @@ class SignifydWebhooks(Controller):
def _case_update(self): def _case_update(self):
data = json.loads(request.httprequest.data) data = json.loads(request.httprequest.data)
vals = request.env['signifyd.connector'].process_post_values(data)
case_id = vals.get('case_id') case_id = data.get('signifydId')
if not case_id:
# Testing webhook
return {'response': 'success'}
case = self._get_case(case_id) case = self._get_case(case_id)
if case: if not case:
case.update_case_info(vals) raise NotFound('CaseId: %s Cannot be found.' % (case_id,))
return {'response': 'success'}
if case_id == 1: case.connector_id._check_webhook_signature(request)
# Special case when verifying webhook.
return {'response': 'success'} case.update_case_info(data)
raise NotFound('CaseId: %s Cannot be found.' % (case_id,)) return {'response': 'success'}
def _get_case(self, case_id): def _get_case(self, case_id):
return request.env['signifyd.case'].sudo().search([('case_id', '=', case_id)], limit=1) return request.env['signifyd.case'].sudo().search([('case_id', '=', case_id)], limit=1)

View File

@@ -73,32 +73,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 def get_decision_vals(self, data=None):
# 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()
# if not self.case_id:
# raise UserError(_('Case not represented in Signifyd.'))
# connector = self._get_connector()
# headers = connector.get_headers()
# r = requests.get(
# connector.API_URL + '/cases/' + str(self.case_id),
# headers=headers
# )
# return r.json()
def get_decision_vals(self):
self.ensure_one() self.ensure_one()
api = self.connector_id.get_connection()
response = api.get_decision(self.ref) if not data:
response.raise_for_status() api = self.connector_id.get_connection()
data = response.json() response = api.get_decision(self.ref)
response.raise_for_status()
data = response.json()
decision = data['decision'] decision = data['decision']
case_vals = { case_vals = {
@@ -116,21 +98,10 @@ class SignifydCase(models.Model):
'snadChargebacks': self.env.ref('website_sale_signifyd.signifyd_coverage_snad').id, 'snadChargebacks': self.env.ref('website_sale_signifyd.signifyd_coverage_snad').id,
'allChargebacks': self.env.ref('website_sale_signifyd.signifyd_coverage_all').id 'allChargebacks': self.env.ref('website_sale_signifyd.signifyd_coverage_all').id
} }
# coverage_ids = []
# if coverage.get('inrChargebacks'):
# coverage_ids += self.env.ref('website_sale_signifyd.signifyd_coverage_inr').ids
# if coverage.get('fraudChargebacks'):
# coverage_ids += self.env.ref('website_sale_signifyd.signifyd_coverage_fraud').ids
# if coverage.get('snadChargebacks'):
# coverage_ids += self.env.ref('website_sale_signifyd.signifyd_coverage_snad').ids
# if coverage.get('allChargebacks'):
# coverage_ids = self.env.ref('website_sale_signifyd.signifyd_coverage_all').ids
# vals['coverage_ids'] = coverage_ids
for name, vals in coverage.items(): for name, vals in coverage.items():
if not vals: if not vals:
continue continue
case_vals['coverage_line_ids'] += [(0, 0, { case_vals['coverage_line_ids'] += [(0, 0, {
# 'case_id': self.id,
'coverage_type_id': coverage_map[name], 'coverage_type_id': coverage_map[name],
'amount': vals.get('amount'), 'amount': vals.get('amount'),
'currency_id': self.env['res.currency'].search( 'currency_id': self.env['res.currency'].search(
@@ -143,47 +114,12 @@ class SignifydCase(models.Model):
for record in self: for record in self:
record.update_case_info() record.update_case_info()
def update_case_info(self, vals=None): def update_case_info(self, data=None):
self.ensure_one() self.ensure_one()
if not self.case_id: if not self.case_id:
raise UserError(_('Case not represented in Signifyd.')) raise UserError(_('Case not represented in Signifyd.'))
if not vals:
vals = self.get_decision_vals()
# case_id = case.get('caseId')
# if not case_id:
# raise ValueError(_('Signifyd Case has no ID?'))
# team_id = case.get('teamId', self.team_id)
# team_name = case.get('teamName', self.team_name)
# ref = case.get('ref', self.ref)
# status = case.get('status', self.status)
# review_disposition = case.get('reviewDisposition', self.review_disposition)
# order_outcome = case.get('orderOutcome', self.order_outcome)
# guarantee_disposition = case.get('guaranteeDisposition', self.guarantee_disposition)
# adjusted_score = case.get('adjustedScore', self.adjusted_score)
# score = case.get('score', self.score)
# checkpoint_action = case.get('checkpointAction', self.checkpoint_action)
# if not checkpoint_action and guarantee_disposition:
# if guarantee_disposition == 'APPROVED':
# checkpoint_action = 'ACCEPT'
# elif guarantee_disposition == 'DECLINED':
# checkpoint_action = 'REJECT'
# else:
# checkpoint_action = 'HOLD'
# vals = { vals = self.get_decision_vals(data)
# 'case_id': case_id,
# 'team_id': team_id,
# 'team_name': team_name,
# 'ref': ref,
# 'status': status,
# 'review_disposition': review_disposition,
# 'order_outcome': order_outcome,
# 'adjusted_score': adjusted_score,
# 'guarantee_disposition': guarantee_disposition,
# 'score': score,
# 'last_update': dt.now(), # why not just use
# 'checkpoint_action': checkpoint_action,
# }
outcome = vals.get('guarantee_disposition') outcome = vals.get('guarantee_disposition')
checkpoint_action = vals.get('checkpoint_action') checkpoint_action = vals.get('checkpoint_action')

View File

@@ -2,9 +2,12 @@
import requests import requests
from datetime import datetime as dt from datetime import datetime as dt
from base64 import b64encode from base64 import b64encode, b64decode
import json import json
import hmac
import hashlib
from odoo import api, fields, models from odoo import api, fields, models
from .signifyd_api import SignifydAPI from .signifyd_api import SignifydAPI
@@ -14,9 +17,7 @@ class SignifydConnector(models.Model):
_description = 'Interact with Signifyd API' _description = 'Interact with Signifyd API'
name = fields.Char(string='Connector Name', required=True) name = fields.Char(string='Connector Name', required=True)
test_mode = fields.Boolean(string='Test Mode')
secret_key = fields.Char(string='API Key', required=True) secret_key = fields.Char(string='API Key', required=True)
secret_key_test = fields.Char(string='TEST API Key')
webhooks_registered = fields.Boolean(string='Successfully Registered Webhooks') webhooks_registered = fields.Boolean(string='Successfully Registered Webhooks')
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')
@@ -88,6 +89,16 @@ class SignifydConnector(models.Model):
self.webhooks_registered = False self.webhooks_registered = False
return notification return notification
def _check_webhook_signature(self, request):
signature = b64decode(request.httprequest.headers.get('SIGNIFYD-SEC-HMAC-SHA256'))
if not signature:
raise Exception('Signifyd webhook missing signature')
key = self.secret_key.encode()
digest = hmac.new(key, request.httprequest.data, hashlib.sha256).digest()
if signature != digest:
raise Exception('Signifyd webhook signature does not match')
def process_post_values(self, post): def process_post_values(self, post):
# Construct dict from request data for endpoints # Construct dict from request data for endpoints
uuid = post.get('uuid') uuid = post.get('uuid')