mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
[REF] website_sale_signifyd: no longer hook on payment controller, improve everything
This commit is contained in:
@@ -9,15 +9,17 @@ Automate Order Fraud Detection with the Signifyd API.
|
|||||||
'website': 'https://hibou.io/',
|
'website': 'https://hibou.io/',
|
||||||
'depends': [
|
'depends': [
|
||||||
'website_sale',
|
'website_sale',
|
||||||
|
'stock',
|
||||||
|
'delivery',
|
||||||
],
|
],
|
||||||
'data': [
|
'data': [
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'views/company_views.xml',
|
|
||||||
'views/partner_views.xml',
|
'views/partner_views.xml',
|
||||||
'views/sale_views.xml',
|
'views/sale_views.xml',
|
||||||
'views/signifyd_views.xml',
|
'views/signifyd_views.xml',
|
||||||
'views/stock_views.xml',
|
'views/stock_views.xml',
|
||||||
'views/web_assets.xml',
|
'views/web_assets.xml',
|
||||||
|
'views/website_views.xml',
|
||||||
],
|
],
|
||||||
'installable': True,
|
'installable': True,
|
||||||
'application': False,
|
'application': False,
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
from . import main
|
|
||||||
from . import signifyd
|
from . import signifyd
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
from odoo.http import request, route
|
|
||||||
from odoo.addons.website_sale.controllers.main import WebsiteSale
|
|
||||||
|
|
||||||
import logging
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class WebsiteSale(WebsiteSale):
|
|
||||||
|
|
||||||
@route(['/shop/confirmation'], type='http', auth="public", website=True, sitemap=False)
|
|
||||||
def payment_confirmation(self, **post):
|
|
||||||
res = super(WebsiteSale, self).payment_confirmation()
|
|
||||||
# order_session_id = request.session.session_token
|
|
||||||
checkout_token = request.session.session_token
|
|
||||||
order_session_id = checkout_token # TODO what is the appropriate variable?
|
|
||||||
_logger.warn(str(request.session))
|
|
||||||
browser_ip_address = request.httprequest.environ['REMOTE_ADDR']
|
|
||||||
sale_order_id = request.session.get('sale_last_order_id')
|
|
||||||
if sale_order_id:
|
|
||||||
order = request.env['sale.order'].sudo().browse(sale_order_id)
|
|
||||||
# Post completed order to Signifyd
|
|
||||||
signifyd = request.env.company.signifyd_connector_id
|
|
||||||
if signifyd:
|
|
||||||
# TODO should the signifyd variable be used?
|
|
||||||
order.post_signifyd_case(order_session_id, checkout_token, browser_ip_address)
|
|
||||||
|
|
||||||
return res
|
|
||||||
@@ -4,36 +4,19 @@ from odoo.http import Response
|
|||||||
|
|
||||||
|
|
||||||
class SignifydWebhooks(Controller):
|
class SignifydWebhooks(Controller):
|
||||||
@route(['/cases/creation'], type='json', auth='public', methods=['POST'], csrf=False)
|
|
||||||
def case_creation(self, *args, **post):
|
|
||||||
data = json.loads(request.httprequest.data)
|
|
||||||
vals = request.env['signifyd.connector'].process_post_values(data)
|
|
||||||
# Update case with info
|
|
||||||
case = request.env['signifyd.case'].sudo().search([('case_id', '=', vals['case_id'])])
|
|
||||||
if case:
|
|
||||||
case.sudo().update_case_info(vals)
|
|
||||||
# Request guarantee for case if eligible
|
|
||||||
try:
|
|
||||||
case.request_guarantee()
|
|
||||||
if case.guarantee_requested and not case.guarantee_eligible:
|
|
||||||
# Only alert Signifyd to stop trying if we have at least tried once already
|
|
||||||
return Response({'response': 'success'}, status=200, mimetype='application/json')
|
|
||||||
# TODO what would the return case be here?
|
|
||||||
except:
|
|
||||||
# Signifyd API will try again up to 15 times if a non-2** code is returned
|
|
||||||
return Response({'response': 'failed'}, status=500, mimetype='application/json')
|
|
||||||
# TODO what would the return case be here?
|
|
||||||
|
|
||||||
@route(['/cases/update'], type='json', auth='public', methods=['POST'], csrf=False)
|
@route(['/signifyd/cases/update'], type='json', auth='public', methods=['POST'], csrf=False, website=True)
|
||||||
def case_update(self, *args, **post):
|
def case_update(self, *args, **post):
|
||||||
|
return self._case_update()
|
||||||
|
|
||||||
|
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)
|
vals = request.env['signifyd.connector'].process_post_values(data)
|
||||||
case = request.env['signifyd.case'].sudo().search([('case_id', '=', vals['case_id'])])
|
case = self._get_case(vals.get('case_id'))
|
||||||
if case:
|
if case:
|
||||||
case.update_case_info(vals)
|
case.update_case_info(vals)
|
||||||
|
return Response({'response': 'success'}, status=200, mimetype='application/json')
|
||||||
|
return Response({'response': 'failed'}, status=500, mimetype='application/json')
|
||||||
|
|
||||||
outcome = vals.get('guarantee_disposition')
|
def _get_case(self, case_id):
|
||||||
if case and outcome == 'DECLINED':
|
return request.env['signifyd.case'].sudo().search([('case_id', '=', case_id)], limit=1)
|
||||||
for user in request.env.company.signifyd_connector_id.notify_user_ids:
|
|
||||||
case.sudo().create_notification(user, outcome)
|
|
||||||
# TODO any return result?
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from . import company
|
|
||||||
from . import partner
|
from . import partner
|
||||||
from . import sale_order
|
from . import sale_order
|
||||||
from . import signifyd
|
from . import signifyd
|
||||||
from . import signifyd_connector
|
from . import signifyd_connector
|
||||||
from . import stock
|
from . import stock
|
||||||
|
from . import website
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
from odoo import fields, models
|
|
||||||
|
|
||||||
|
|
||||||
class ResCompany(models.Model):
|
|
||||||
_inherit = 'res.company'
|
|
||||||
|
|
||||||
# TODO move to website
|
|
||||||
signifyd_connector_id = fields.Many2one('signifyd.connector')
|
|
||||||
@@ -8,6 +8,7 @@ class ResPartner(models.Model):
|
|||||||
signifyd_case_count = fields.Integer(compute='_compute_signifyd_stats', string='Signifyd Cases')
|
signifyd_case_count = fields.Integer(compute='_compute_signifyd_stats', string='Signifyd Cases')
|
||||||
signifyd_average_score = fields.Float(compute='_compute_signifyd_stats', string='Signifyd Score')
|
signifyd_average_score = fields.Float(compute='_compute_signifyd_stats', string='Signifyd Score')
|
||||||
|
|
||||||
|
@api.depends('signifyd_case_ids')
|
||||||
def _compute_signifyd_stats(self):
|
def _compute_signifyd_stats(self):
|
||||||
for record in self:
|
for record in self:
|
||||||
cases = record.signifyd_case_ids
|
cases = record.signifyd_case_ids
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
from odoo import api, fields, models
|
from odoo import api, fields, models
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
from odoo.http import request
|
||||||
|
|
||||||
|
|
||||||
class SaleOrder(models.Model):
|
class SaleOrder(models.Model):
|
||||||
_inherit = 'sale.order'
|
_inherit = 'sale.order'
|
||||||
|
|
||||||
signifyd_case_id = fields.Many2one('signifyd.case', readonly=1)
|
signifyd_case_id = fields.Many2one('signifyd.case', readonly=1, copy=False)
|
||||||
singifyd_score = fields.Float(related='signifyd_case_id.score')
|
singifyd_score = fields.Float(related='signifyd_case_id.score')
|
||||||
signifyd_disposition_status = fields.Selection(related='signifyd_case_id.guarantee_disposition')
|
signifyd_disposition_status = fields.Selection(related='signifyd_case_id.guarantee_disposition')
|
||||||
|
|
||||||
def action_view_signifyd_case(self):
|
def action_view_signifyd_case(self):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
if not self.signifyd_case_id:
|
||||||
|
raise UserError('This order has no Signifyd Case')
|
||||||
form_id = self.env.ref('website_sale_signifyd.signifyd_case_form_view').id
|
form_id = self.env.ref('website_sale_signifyd.signifyd_case_form_view').id
|
||||||
context = {'create': False, 'delete': False}
|
context = {'create': False, 'delete': False}
|
||||||
return {
|
return {
|
||||||
@@ -22,128 +26,126 @@ class SaleOrder(models.Model):
|
|||||||
'context': context,
|
'context': context,
|
||||||
}
|
}
|
||||||
|
|
||||||
def post_signifyd_case(self, order_session_id, checkout_token, browser_ip_address):
|
def action_confirm(self):
|
||||||
|
res = super().action_confirm()
|
||||||
|
for sale in self.filtered(lambda so: so.state in ('sale', 'done') and not so.signifyd_case_id):
|
||||||
|
_case = sale.post_signifyd_case()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def post_signifyd_case(self):
|
||||||
|
if not self.website_id.signifyd_connector_id:
|
||||||
|
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
|
# Session values for Signifyd post
|
||||||
data = {
|
sig_vals = self._prepare_signifyd_case_values(order_session_id, checkout_token, browser_ip_address)
|
||||||
'order_session_id': order_session_id,
|
|
||||||
'checkout_token': checkout_token,
|
|
||||||
'browser_ip_address': browser_ip_address,
|
|
||||||
}
|
|
||||||
sig_vals = self.prepare_signifyd_case_values(data)
|
|
||||||
|
|
||||||
case_res = self.env['signifyd.case'].post_case(sig_vals)
|
case = self.env['signifyd.case'].post_case(self.website_id.signifyd_connector_id, sig_vals)
|
||||||
|
|
||||||
success_response = case_res.get('investigationId')
|
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,
|
||||||
'case_id': success_response,
|
'case_id': success_response,
|
||||||
'name': success_response,
|
'name': success_response,
|
||||||
|
'partner_id': self.partner_id.id,
|
||||||
})
|
})
|
||||||
self.write({'signifyd_case_id': new_case.id})
|
self.write({'signifyd_case_id': new_case.id})
|
||||||
self.partner_id.write({
|
|
||||||
'signifyd_case_ids': [(4, new_case.id)],
|
|
||||||
})
|
|
||||||
return new_case
|
return new_case
|
||||||
|
# TODO do we need to raise an exception?
|
||||||
|
return None
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def prepare_signifyd_case_values(self, data):
|
def _prepare_signifyd_case_values(self, order_session_id, checkout_token, browser_ip_address):
|
||||||
order_session_id = data.get('order_session_id')
|
tx_status_type = {
|
||||||
checkout_token = data.get('checkout_token')
|
'draft': 'FAILURE',
|
||||||
browser_ip_address = data.get('browser_ip_address')
|
'pending': 'PENDING',
|
||||||
|
'authorized': 'SUCCESS',
|
||||||
new_case_vals = {}
|
'done': 'SUCCESS',
|
||||||
|
'cancel': 'FAILURE',
|
||||||
new_case_vals['purchase'] = {
|
'error': 'ERROR',
|
||||||
"orderSessionId": order_session_id,
|
|
||||||
"orderId": self.id,
|
|
||||||
"checkoutToken": checkout_token,
|
|
||||||
"browserIpAddress": browser_ip_address,
|
|
||||||
"currency": self.partner_id.currency_id.name,
|
|
||||||
"orderChannel": "WEB",
|
|
||||||
"totalPrice": self.amount_total,
|
|
||||||
}
|
}
|
||||||
|
recipients = self.partner_invoice_id + self.partner_shipping_id
|
||||||
new_case_vals['purchase']['products'] = []
|
new_case_vals = {
|
||||||
for line in self.order_line:
|
'decisionRequest': {
|
||||||
product = line.product_id
|
'paymentFraud': 'GUARANTEE',
|
||||||
vals = {
|
},
|
||||||
"itemId": product.id,
|
'purchase': {
|
||||||
"itemName": product.name,
|
"orderSessionId": order_session_id,
|
||||||
"itemIsDigital": False,
|
"orderId": self.id,
|
||||||
"itemCategory": product.categ_id.name,
|
"checkoutToken": checkout_token,
|
||||||
"itemUrl": product.website_url,
|
"browserIpAddress": browser_ip_address,
|
||||||
"itemQuantity": line.product_uom_qty,
|
|
||||||
"itemPrice": line.price_unit,
|
|
||||||
"itemWeight": product.weight,
|
|
||||||
}
|
|
||||||
new_case_vals['purchase']['products'].append(vals)
|
|
||||||
|
|
||||||
new_case_vals['purchase']['shipments'] = []
|
|
||||||
if self.carrier_id:
|
|
||||||
vals = {
|
|
||||||
"shipper": self.carrier_id.name,
|
|
||||||
"shippingMethod": "ground",
|
|
||||||
"shippingPrice": self.amount_delivery,
|
|
||||||
}
|
|
||||||
new_case_vals['purchase']['shipments'].append(vals)
|
|
||||||
|
|
||||||
new_case_vals['recipients'] = []
|
|
||||||
recipients = [self.partner_invoice_id, self.partner_shipping_id]
|
|
||||||
for partner in recipients:
|
|
||||||
vals = {
|
|
||||||
"fullName": partner.name,
|
|
||||||
"confirmationEmail": partner.email,
|
|
||||||
"confirmationPhone": partner.phone,
|
|
||||||
"organization": partner.company_id.name,
|
|
||||||
"deliveryAddress": {
|
|
||||||
"streetAddress": partner.street,
|
|
||||||
"unit": partner.street2,
|
|
||||||
"city": partner.city,
|
|
||||||
"provinceCode": partner.state_id.code,
|
|
||||||
"postalCode": partner.zip,
|
|
||||||
"countryCode": partner.country_id.code,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
new_case_vals['recipients'].append(vals)
|
|
||||||
|
|
||||||
new_case_vals['transactions'] = []
|
|
||||||
# payment.transaction
|
|
||||||
for tx in self.transaction_ids:
|
|
||||||
tx_status_type = {
|
|
||||||
'draft': 'FAILURE',
|
|
||||||
'pending': 'PENDING',
|
|
||||||
'authorized': 'SUCCESS',
|
|
||||||
'done': 'SUCCESS',
|
|
||||||
'cancel': 'FAILURE',
|
|
||||||
'error': 'ERROR',
|
|
||||||
}
|
|
||||||
|
|
||||||
tx_status = tx_status_type[tx.state]
|
|
||||||
|
|
||||||
vals = {
|
|
||||||
"parentTransactionId": None,
|
|
||||||
"transactionId": tx.id,
|
|
||||||
"gateway": tx.acquirer_id.name,
|
|
||||||
"paymentMethod": "CREDIT_CARD",
|
|
||||||
"gatewayStatusCode": tx_status,
|
|
||||||
"type": "AUTHORIZATION",
|
|
||||||
"currency": self.partner_id.currency_id.name,
|
"currency": self.partner_id.currency_id.name,
|
||||||
"amount": tx.amount,
|
"orderChannel": "WEB",
|
||||||
"avsResponseCode": "Y",
|
"totalPrice": self.amount_total,
|
||||||
"cvvResponseCode": "N",
|
'products': [
|
||||||
"checkoutPaymentDetails": {
|
{
|
||||||
"holderName": tx.partner_id.name,
|
"itemId": line.product_id.id,
|
||||||
"billingAddress": {
|
"itemName": line.product_id.name,
|
||||||
"streetAddress": tx.partner_id.street,
|
"itemIsDigital": False,
|
||||||
"unit": tx.partner_id.street2,
|
"itemCategory": line.product_id.categ_id.name,
|
||||||
"city": tx.partner_id.city,
|
"itemUrl": line.product_id.website_url or '',
|
||||||
"provinceCode": tx.partner_id.state_id.code,
|
"itemQuantity": line.product_uom_qty,
|
||||||
"postalCode": tx.partner_id.zip,
|
"itemPrice": line.price_unit,
|
||||||
"countryCode": tx.partner_id.country_id.code,
|
"itemWeight": line.product_id.weight or 0.1,
|
||||||
|
}
|
||||||
|
for line in self.order_line if line.product_id
|
||||||
|
],
|
||||||
|
'shipments': [{
|
||||||
|
"shipper": carrier.name,
|
||||||
|
"shippingMethod": "ground",
|
||||||
|
"shippingPrice": self.amount_delivery,
|
||||||
|
}
|
||||||
|
for carrier in self.carrier_id
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'recipients': [
|
||||||
|
{
|
||||||
|
"fullName": partner.name,
|
||||||
|
"confirmationEmail": partner.email,
|
||||||
|
"confirmationPhone": partner.phone,
|
||||||
|
"organization": partner.company_id.name,
|
||||||
|
"deliveryAddress": {
|
||||||
|
"streetAddress": partner.street,
|
||||||
|
"unit": partner.street2,
|
||||||
|
"city": partner.city,
|
||||||
|
"provinceCode": partner.state_id.code,
|
||||||
|
"postalCode": partner.zip,
|
||||||
|
"countryCode": partner.country_id.code,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
for partner in recipients
|
||||||
new_case_vals['transactions'].append(vals)
|
],
|
||||||
|
'transactions': [
|
||||||
|
{
|
||||||
|
"parentTransactionId": None,
|
||||||
|
"transactionId": tx.id,
|
||||||
|
"gateway": tx.acquirer_id.name,
|
||||||
|
"paymentMethod": "CREDIT_CARD",
|
||||||
|
"gatewayStatusCode": tx_status_type.get(tx.state, 'PENDING'),
|
||||||
|
"type": "AUTHORIZATION",
|
||||||
|
"currency": self.partner_id.currency_id.name,
|
||||||
|
"amount": tx.amount,
|
||||||
|
# "avsResponseCode": "Y",
|
||||||
|
# "cvvResponseCode": "N",
|
||||||
|
"checkoutPaymentDetails": {
|
||||||
|
"holderName": tx.partner_id.name,
|
||||||
|
"billingAddress": {
|
||||||
|
"streetAddress": tx.partner_id.street,
|
||||||
|
"unit": tx.partner_id.street2,
|
||||||
|
"city": tx.partner_id.city,
|
||||||
|
"provinceCode": tx.partner_id.state_id.code,
|
||||||
|
"postalCode": tx.partner_id.zip,
|
||||||
|
"countryCode": tx.partner_id.country_id.code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for tx in self.transaction_ids
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
return new_case_vals
|
return new_case_vals
|
||||||
|
|||||||
@@ -42,97 +42,77 @@ class SignifydCase(models.Model):
|
|||||||
('CANCELED', 'Canceled'),
|
('CANCELED', 'Canceled'),
|
||||||
], string='Guarantee Status')
|
], string='Guarantee Status')
|
||||||
disposition_reason = fields.Char('Disposition Reason')
|
disposition_reason = fields.Char('Disposition Reason')
|
||||||
guarantee_eligible = fields.Boolean('Eligible for Guarantee')
|
|
||||||
guarantee_requested = fields.Boolean('Requested Guarantee')
|
|
||||||
score = fields.Float(string='Transaction Score')
|
score = fields.Float(string='Transaction Score')
|
||||||
adjusted_score = fields.Float(string='Adjusted Score')
|
adjusted_score = fields.Float(string='Adjusted Score')
|
||||||
signifyd_url = fields.Char('Signifyd.com', compute='_compute_signifyd_url')
|
signifyd_url = fields.Char('Signifyd.com', compute='_compute_signifyd_url')
|
||||||
|
|
||||||
|
def _get_connector(self):
|
||||||
|
return self.order_id.website_id.signifyd_connector_id
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _compute_signifyd_url(self):
|
def _compute_signifyd_url(self):
|
||||||
for record in self:
|
for record in self:
|
||||||
if record.case_id:
|
if record.case_id:
|
||||||
self.signifyd_url = 'https://app.signifyd.com/cases/%s' % record.case_id
|
self.signifyd_url = 'https://app.signifyd.com/cases/%s' % (record.case_id, )
|
||||||
else:
|
else:
|
||||||
self.signifyd_url = ''
|
self.signifyd_url = ''
|
||||||
|
|
||||||
def write(self, vals):
|
def write(self, vals):
|
||||||
original_disposition = {c: c.guarantee_disposition for c in self}
|
original_disposition = {c: c.guarantee_disposition for c in self}
|
||||||
res = super(SignifydCase, self).write(vals)
|
res = super(SignifydCase, self).write(vals)
|
||||||
disposition = vals.get('guarantee_disposition')
|
disposition = vals.get('guarantee_disposition', False)
|
||||||
for case in self:
|
for case in self.filtered(lambda c: c.order_id and original_disposition[c] != disposition):
|
||||||
if case.order_id and original_disposition[case] != disposition:
|
case.order_id.message_post(body=_('Signifyd Updated Record to %s' % disposition),
|
||||||
self.order_id.message_post(body=_('Signifyd Updated Record to %s' % vals['guarantee_disposition']),
|
subtype='website_sale_signifyd.disposition_change')
|
||||||
subtype='website_sale_signifyd.disposition_change')
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def post_case(self, values):
|
def post_case(self, connector, values):
|
||||||
signifyd = self.env['signifyd.connector'] # TODO HOW, this shouldn't be a singleton
|
headers = connector.get_headers()
|
||||||
headers = signifyd.get_headers()
|
|
||||||
data = json.dumps(values, indent=4, sort_keys=True, default=str)
|
data = json.dumps(values, indent=4, sort_keys=True, default=str)
|
||||||
|
|
||||||
# TODO this should be in `signifyd.connector`
|
# TODO this should be in `signifyd.connector`
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
signifyd.API_URL + '/cases',
|
connector.API_URL + '/cases',
|
||||||
headers=headers,
|
headers=headers,
|
||||||
data=data,
|
data=data,
|
||||||
)
|
)
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
@api.model
|
|
||||||
def get_case(self):
|
def get_case(self):
|
||||||
# TODO See above....
|
self.ensure_one()
|
||||||
signifyd = self.env['signifyd.connector']
|
if not self.case_id:
|
||||||
headers = signifyd.get_headers()
|
raise UserError('Case not represented in Signifyd.')
|
||||||
|
connector = self._get_connector()
|
||||||
|
headers = connector.get_headers()
|
||||||
r = requests.get(
|
r = requests.get(
|
||||||
signifyd.API_URL + '/cases/' + str(self.case_id),
|
connector.API_URL + '/cases/' + str(self.case_id),
|
||||||
headers=headers
|
headers=headers
|
||||||
)
|
)
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
@api.model
|
|
||||||
def request_guarantee(self, *args):
|
|
||||||
# TODO See above....
|
|
||||||
signifyd = self.env['signifyd.connector']
|
|
||||||
headers = signifyd.get_headers()
|
|
||||||
values = json.dumps({"caseId": self.case_id})
|
|
||||||
r = requests.post(
|
|
||||||
signifyd.API_URL + '/async/guarantees',
|
|
||||||
headers=headers,
|
|
||||||
data=values,
|
|
||||||
)
|
|
||||||
|
|
||||||
if 200 <= r.status_code < 300:
|
|
||||||
self.write({'guarantee_requested': True})
|
|
||||||
else:
|
|
||||||
msg = r.content.decode("utf-8")
|
|
||||||
raise UserError(_(msg))
|
|
||||||
|
|
||||||
def action_request_guarantee(self):
|
|
||||||
for record in self:
|
|
||||||
record.request_guarantee()
|
|
||||||
|
|
||||||
def action_force_update_case(self):
|
def action_force_update_case(self):
|
||||||
for record in self:
|
for record in self:
|
||||||
record.update_case_info()
|
record.update_case_info()
|
||||||
|
|
||||||
@api.model
|
|
||||||
def update_case_info(self, vals=None):
|
def update_case_info(self, vals=None):
|
||||||
|
self.ensure_one()
|
||||||
|
if not self.case_id:
|
||||||
|
raise UserError('Case not represented in Signifyd.')
|
||||||
if not vals:
|
if not vals:
|
||||||
case = self.get_case()
|
case = self.get_case()
|
||||||
case_id = case.get('caseId')
|
case_id = case.get('caseId')
|
||||||
team_id = case.get('teamId')
|
if not case_id:
|
||||||
team_name = case.get('teamName')
|
raise ValueError('Signifyd Case has no ID?')
|
||||||
uuid = case.get('uuid')
|
team_id = case.get('teamId', self.team_id)
|
||||||
status = case.get('status')
|
team_name = case.get('teamName', self.team_name)
|
||||||
review_disposition = case.get('reviewDisposition')
|
uuid = case.get('uuid', self.uuid)
|
||||||
order_outcome = case.get('orderOutcome')
|
status = case.get('status', self.status)
|
||||||
guarantee_disposition = case.get('guaranteeDisposition')
|
review_disposition = case.get('reviewDisposition', self.review_disposition)
|
||||||
adjusted_score = case.get('adjustedScore')
|
order_outcome = case.get('orderOutcome', self.order_outcome)
|
||||||
score = case.get('score')
|
guarantee_disposition = case.get('guaranteeDisposition', self.guarantee_disposition)
|
||||||
guarantee_eligible = case.get('guaranteeEligible')
|
adjusted_score = case.get('adjustedScore', self.adjusted_score)
|
||||||
# order_id = case.get('orderId')
|
score = case.get('score', self.score)
|
||||||
|
|
||||||
vals = {
|
vals = {
|
||||||
'case_id': case_id,
|
'case_id': case_id,
|
||||||
@@ -145,13 +125,13 @@ class SignifydCase(models.Model):
|
|||||||
'adjusted_score': adjusted_score,
|
'adjusted_score': adjusted_score,
|
||||||
'guarantee_disposition': guarantee_disposition,
|
'guarantee_disposition': guarantee_disposition,
|
||||||
'score': score,
|
'score': score,
|
||||||
'guarantee_eligible': guarantee_eligible,
|
'last_update': dt.now(), # why not just use
|
||||||
'last_update': dt.now(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
outcome = vals.get('guarantee_disposition')
|
outcome = vals.get('guarantee_disposition')
|
||||||
if outcome == 'DECLINED':
|
if outcome == 'DECLINED':
|
||||||
for user in self.env.company.signifyd_connector_id.notify_user_ids:
|
connector = self._get_connector()
|
||||||
|
for user in connector.notify_user_ids:
|
||||||
self.create_notification(user, outcome)
|
self.create_notification(user, outcome)
|
||||||
|
|
||||||
self.write(vals)
|
self.write(vals)
|
||||||
@@ -163,6 +143,6 @@ class SignifydCase(models.Model):
|
|||||||
'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id,
|
'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id,
|
||||||
'user_id': user.id,
|
'user_id': user.id,
|
||||||
'res_id': self.order_id.id,
|
'res_id': self.order_id.id,
|
||||||
'res_model_id': self.env['ir.model']._get('sale.order').id,
|
'res_model_id': self.env['ir.model']._get(self.order_id._name).id,
|
||||||
}
|
}
|
||||||
self.env['mail.activity'].create(vals)
|
self.env['mail.activity'].create(vals)
|
||||||
|
|||||||
@@ -12,56 +12,65 @@ class SignifydConnector(models.Model):
|
|||||||
|
|
||||||
name = fields.Char(string='Connector Name')
|
name = fields.Char(string='Connector Name')
|
||||||
test_mode = fields.Boolean(string='Test Mode')
|
test_mode = fields.Boolean(string='Test Mode')
|
||||||
user_key = fields.Char(string='Username')
|
user_key = fields.Char(string='Team/Username')
|
||||||
secret_key = fields.Char(string='API Key')
|
secret_key = fields.Char(string='API Key')
|
||||||
user_key_test = fields.Char(string='TEST Username')
|
user_key_test = fields.Char(string='TEST Team/Username')
|
||||||
secret_key_test = fields.Char(string='TEST API Key')
|
secret_key_test = fields.Char(string='TEST API Key')
|
||||||
open_so_cap = fields.Integer(string='Cap requests at:')
|
|
||||||
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 event notifications')
|
notify_user_ids = fields.Many2many('res.users', string='Receive decline notifications')
|
||||||
|
|
||||||
|
# TODO ideally this would be a regular constant
|
||||||
|
# however other entities currently use this by reference
|
||||||
API_URL = 'https://api.signifyd.com/v2'
|
API_URL = 'https://api.signifyd.com/v2'
|
||||||
|
|
||||||
def get_headers(self):
|
def get_headers(self):
|
||||||
|
self.ensure_one()
|
||||||
# Check for prod or test mode
|
# Check for prod or test mode
|
||||||
signifyd = self.env.company.signifyd_connector_id
|
if self.test_mode:
|
||||||
if not signifyd:
|
api_key = self.secret_key_test
|
||||||
return False
|
|
||||||
|
|
||||||
if signifyd.test_mode:
|
|
||||||
api_key = signifyd.secret_key_test
|
|
||||||
else:
|
else:
|
||||||
api_key = signifyd.secret_key
|
api_key = self.secret_key
|
||||||
|
|
||||||
b64_auth_key = b64encode(api_key.encode('utf-8'))
|
b64_auth_key = b64encode(api_key.encode()).decode().replace('=', '')
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
'Authorization': 'Basic ' + str(b64_auth_key, 'utf-8').replace('=', ''),
|
'Authorization': 'Basic ' + b64_auth_key,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
}
|
}
|
||||||
|
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
def register_webhooks(self):
|
def register_webhooks(self):
|
||||||
|
self.ensure_one()
|
||||||
headers = self.get_headers()
|
headers = self.get_headers()
|
||||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
# This should come from the website...
|
||||||
|
# we may need a better way to link the connector to the website.
|
||||||
|
base_url = None
|
||||||
|
website = self.env['website'].search([('signifyd_connector_id', '=', self.id)], limit=1)
|
||||||
|
if website and website.domain:
|
||||||
|
base_url = website.domain
|
||||||
|
if base_url.find('://') <= 0: # documentation says if no protocol use http
|
||||||
|
base_url = 'http://' + base_url
|
||||||
|
if not 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
|
||||||
"event": "CASE_CREATION",
|
# {
|
||||||
"url": base_url + "/cases/creation"
|
# "event": "CASE_CREATION",
|
||||||
},
|
# "url": base_url + "/signifyd/cases/update"
|
||||||
|
# },
|
||||||
{
|
{
|
||||||
"event": "CASE_RESCORE",
|
"event": "CASE_RESCORE",
|
||||||
"url": base_url + "/cases/update"
|
"url": base_url + "/signifyd/cases/update"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"event": "CASE_REVIEW",
|
"event": "CASE_REVIEW",
|
||||||
"url": base_url + "/cases/update"
|
"url": base_url + "/signifyd/cases/update"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"event": "GUARANTEE_COMPLETION",
|
"event": "GUARANTEE_COMPLETION",
|
||||||
"url": base_url + "/cases/update"
|
"url": base_url + "/signifyd/cases/update"
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -75,7 +84,6 @@ class SignifydConnector(models.Model):
|
|||||||
return r
|
return r
|
||||||
|
|
||||||
def action_register_webhooks(self):
|
def action_register_webhooks(self):
|
||||||
|
|
||||||
notification = {
|
notification = {
|
||||||
'type': 'ir.actions.client',
|
'type': 'ir.actions.client',
|
||||||
'tag': 'display_notification',
|
'tag': 'display_notification',
|
||||||
@@ -96,11 +104,11 @@ class SignifydConnector(models.Model):
|
|||||||
else:
|
else:
|
||||||
notification['params']['type'] = 'danger'
|
notification['params']['type'] = 'danger'
|
||||||
notification['params']['message'] = res.content.decode('utf-8')
|
notification['params']['message'] = res.content.decode('utf-8')
|
||||||
|
self.webhooks_registered = False
|
||||||
return notification
|
return notification
|
||||||
|
|
||||||
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
|
||||||
guarantee_eligible = post.get('guaranteeEligible')
|
|
||||||
uuid = post.get('uuid')
|
uuid = post.get('uuid')
|
||||||
case_id = post.get('caseId')
|
case_id = post.get('caseId')
|
||||||
team_name = post.get('teamName')
|
team_name = post.get('teamName')
|
||||||
@@ -117,7 +125,6 @@ class SignifydConnector(models.Model):
|
|||||||
values = {}
|
values = {}
|
||||||
|
|
||||||
# Validate that the order and case match the request
|
# Validate that the order and case match the request
|
||||||
values.update({'guarantee_eligible': guarantee_eligible}) if guarantee_eligible else ''
|
|
||||||
values.update({'uuid': uuid}) if uuid else ''
|
values.update({'uuid': uuid}) if uuid else ''
|
||||||
values.update({'team_name': team_name}) if team_name else ''
|
values.update({'team_name': team_name}) if team_name else ''
|
||||||
values.update({'team_id': team_id}) if team_id else ''
|
values.update({'team_id': team_id}) if team_id else ''
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from odoo import fields, models
|
from odoo import fields, models
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
class StockPicking(models.Model):
|
class StockPicking(models.Model):
|
||||||
_inherit = 'stock.picking'
|
_inherit = 'stock.picking'
|
||||||
@@ -9,6 +9,8 @@ class StockPicking(models.Model):
|
|||||||
|
|
||||||
def action_view_signifyd_case(self):
|
def action_view_signifyd_case(self):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
if not self.singifyd_case_id:
|
||||||
|
raise UserError('No Signifyd Case')
|
||||||
form_id = self.env.ref('website_sale_signifyd.signifyd_case_form_view').id
|
form_id = self.env.ref('website_sale_signifyd.signifyd_case_form_view').id
|
||||||
context = {'create': False, 'delete': False, 'id': self.sale_id.signifyd_case_id.id}
|
context = {'create': False, 'delete': False, 'id': self.sale_id.signifyd_case_id.id}
|
||||||
return {
|
return {
|
||||||
|
|||||||
7
website_sale_signifyd/models/website.py
Normal file
7
website_sale_signifyd/models/website.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class Website(models.Model):
|
||||||
|
_inherit = 'website'
|
||||||
|
|
||||||
|
signifyd_connector_id = fields.Many2one('signifyd.connector')
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
manage_signifyd_connector,manage_signifyd_connector,model_signifyd_connector,base.group_erp_manager,1,1,1,1
|
manage_signifyd_connector,manage_signifyd_connector,model_signifyd_connector,base.group_erp_manager,1,1,1,1
|
||||||
access_signifyd_connector,access_signifyd_connector,model_signifyd_connector,base.group_user,1,1,1,0
|
access_signifyd_connector,access_signifyd_connector,model_signifyd_connector,base.group_user,1,0,0,0
|
||||||
public_signifyd_connector,public_signifyd_connector,model_signifyd_connector,base.group_public,1,1,1,0
|
public_signifyd_connector,public_signifyd_connector,model_signifyd_connector,base.group_public,1,0,0,0
|
||||||
portal_signifyd_connector,portal_signifyd_connector,model_signifyd_connector,base.group_portal,1,1,1,0
|
portal_signifyd_connector,portal_signifyd_connector,model_signifyd_connector,base.group_portal,1,0,0,0
|
||||||
manage_signifyd_case,manage_signifyd_case,model_signifyd_case,base.group_erp_manager,1,1,1,1
|
manage_signifyd_case,manage_signifyd_case,model_signifyd_case,base.group_erp_manager,1,1,1,1
|
||||||
access_signifyd_case,access_signifyd_case,model_signifyd_case,base.group_user,1,1,1,0
|
access_signifyd_case,access_signifyd_case,model_signifyd_case,base.group_user,1,0,0,0
|
||||||
public_signifyd_case,public_signifyd_case,model_signifyd_case,base.group_public,1,1,1,0
|
public_signifyd_case,public_signifyd_case,model_signifyd_case,base.group_public,1,0,0,0
|
||||||
portal_signifyd_case,portal_signifyd_case,model_signifyd_case,base.group_portal,1,1,1,0
|
portal_signifyd_case,portal_signifyd_case,model_signifyd_case,base.group_portal,1,0,0,0
|
||||||
|
@@ -1,23 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<odoo>
|
|
||||||
|
|
||||||
<record id="res_company_form_view_inherit" model="ir.ui.view">
|
|
||||||
<field name="name">res.company.form.inherit</field>
|
|
||||||
<field name="model">res.company</field>
|
|
||||||
<field name="inherit_id" ref="base.view_company_form"/>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<xpath expr="//notebook//page[last()]" position="after">
|
|
||||||
<page name="signifyd" string="Signifyd">
|
|
||||||
<div class="oe_title">
|
|
||||||
<h3>
|
|
||||||
Connector
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<label for="signifyd_connector_id" string="Settings"/>
|
|
||||||
<field name="signifyd_connector_id"/>
|
|
||||||
</page>
|
|
||||||
</xpath>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
</odoo>
|
|
||||||
@@ -26,14 +26,7 @@
|
|||||||
attrs="{'invisible': [('score', '>', 300)]}">
|
attrs="{'invisible': [('score', '>', 300)]}">
|
||||||
<field string="Score" name="score" widget="statinfo"/>
|
<field string="Score" name="score" widget="statinfo"/>
|
||||||
</button>
|
</button>
|
||||||
<button class="oe_stat_button text-success"
|
|
||||||
icon="fa-share-square-o"
|
|
||||||
type="object"
|
|
||||||
name="action_request_guarantee"
|
|
||||||
string="Request Guarantee"
|
|
||||||
attrs="{'invisible': [ '|', ('guarantee_eligible', '=', False), ('guarantee_requested', '=', True)]}">
|
|
||||||
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="oe_title">
|
<div class="oe_title">
|
||||||
<h1>
|
<h1>
|
||||||
@@ -50,8 +43,6 @@
|
|||||||
<field name="status"/>
|
<field name="status"/>
|
||||||
<field name="order_outcome"/>
|
<field name="order_outcome"/>
|
||||||
<field name="review_disposition"/>
|
<field name="review_disposition"/>
|
||||||
<field name="guarantee_eligible"/>
|
|
||||||
<field name="guarantee_requested"/>
|
|
||||||
<field name="guarantee_disposition"/>
|
<field name="guarantee_disposition"/>
|
||||||
<field name="disposition_reason"/>
|
<field name="disposition_reason"/>
|
||||||
</group>
|
</group>
|
||||||
@@ -79,6 +70,9 @@
|
|||||||
<button name="action_register_webhooks" type="object"
|
<button name="action_register_webhooks" type="object"
|
||||||
string="Register Webhooks" class="oe_highlight"
|
string="Register Webhooks" class="oe_highlight"
|
||||||
attrs="{'invisible': [('webhooks_registered', '=', True)]}"/>
|
attrs="{'invisible': [('webhooks_registered', '=', True)]}"/>
|
||||||
|
<button name="action_register_webhooks" type="object"
|
||||||
|
string="Re-register Webhooks"
|
||||||
|
attrs="{'invisible': [('webhooks_registered', '=', False)]}"/>
|
||||||
</header>
|
</header>
|
||||||
<sheet>
|
<sheet>
|
||||||
|
|
||||||
@@ -125,4 +119,29 @@
|
|||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<record id="signifyd_connector_tree_view" model="ir.ui.view">
|
||||||
|
<field name="name">signifyd.tree.view</field>
|
||||||
|
<field name="model">signifyd.connector</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree string="Signifyd Connectors">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="test_mode"/>
|
||||||
|
<field name="webhooks_registered"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_signifyd_connector" model="ir.actions.act_window">
|
||||||
|
<field name="name">Signifyd Connectors</field>
|
||||||
|
<field name="res_model">signifyd.connector</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem
|
||||||
|
name="Signifyd"
|
||||||
|
parent="website.menu_website_global_configuration"
|
||||||
|
action="action_signifyd_connector"
|
||||||
|
id="menu_action_signifyd_connector"
|
||||||
|
sequence="10" />
|
||||||
|
|
||||||
</odoo>
|
</odoo>
|
||||||
21
website_sale_signifyd/views/website_views.xml
Normal file
21
website_sale_signifyd/views/website_views.xml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_website_form_inherit" model="ir.ui.view">
|
||||||
|
<field name="name">website.form.inherit</field>
|
||||||
|
<field name="model">website</field>
|
||||||
|
<field name="inherit_id" ref="website.view_website_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//group[@name='other']" position="inside">
|
||||||
|
<div class="oe_title">
|
||||||
|
<h3>
|
||||||
|
Signifyd Configuration
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<label for="signifyd_connector_id" string="Settings"/>
|
||||||
|
<field name="signifyd_connector_id"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
Reference in New Issue
Block a user