[IMP][H4547] website_sale_signifyd: API v3 (WIP)

This commit is contained in:
Milan Cosnefroy
2024-10-09 01:03:58 +02:00
parent 7bee4c8a39
commit 8098cfed01
9 changed files with 144 additions and 112 deletions

View File

@@ -18,10 +18,12 @@
<field name="name">All</field> <field name="name">All</field>
<field name="description">Use when you need a financial guarantee on all chargebacks.</field> <field name="description">Use when you need a financial guarantee on all chargebacks.</field>
<field name="code">ALL</field> <field name="code">ALL</field>
<field name="exclusive">True</field>
</record> </record>
<record id="signifyd_coverage_none" model="signifyd.coverage"> <record id="signifyd_coverage_none" model="signifyd.coverage">
<field name="name">None</field> <field name="name">None</field>
<field name="description">Use when you do not need a financial guarantee.</field> <field name="description">Use when you do not need a financial guarantee.</field>
<field name="code">NONE</field> <field name="code">NONE</field>
<field name="exclusive">True</field>
</record> </record>
</odoo> </odoo>

View File

@@ -1,12 +1,12 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. # Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import delivery_carrier from . import delivery_carrier
from . import partner from . import res_partner
from . import payment from . import payment_acquirer
from . import product_template from . import product_template
from . import sale_order from . import sale_order
from . import signifyd_case from . import signifyd_case
from . import signifyd_coverage from . import signifyd_coverage
from . import signifyd_connector from . import signifyd_connector
from . import stock from . import stock_picking
from . import website from . import website

View File

@@ -1,17 +0,0 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import api, fields, models
class PaymentAcquirer(models.Model):
_inherit = 'payment.acquirer'
# TODO: remove options no longer available in api v3
signifyd_case_type = fields.Selection([
('', 'No Case'),
('SCORE', 'Score'),
('DECISION', 'Decision'),
('GUARANTEE', 'Guarantee'),
], string='Signifyd Case Creation', default='')
signify_coverage_types = fields.Many2many('signifyd.coverage', string='Signifyd Coverage Types')

View File

@@ -0,0 +1,27 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import api, fields, models
class PaymentAcquirer(models.Model):
_inherit = 'payment.acquirer'
# TODO: remove options no longer available in api v3
signifyd_case_type = fields.Selection([
('', 'No Case'),
('SCORE', 'Score'),
('DECISION', 'Decision'),
('GUARANTEE', 'Guarantee'),
], string='Signifyd Case Creation', default='')
signifyd_case_required = fields.Boolean(string='Create Signifyd Case', default=True)
signifyd_coverage_ids = fields.Many2many('signifyd.coverage', string='Available Coverage Types',
help='Note that exclusive coverage types will only allow one to be selected.')
@api.onchange('signifyd_coverage_ids')
def _onchange_signifyd_coverage_ids(self):
self.signifyd_coverage_ids = self.signifyd_coverage_ids._apply_exclusivity()
@api.onchange('signifyd_case_required')
def _onchange_signifyd_case_required(self):
if not self.signifyd_case_required:
self.signifyd_coverage_ids = False

View File

@@ -37,19 +37,10 @@ class SaleOrder(models.Model):
def _should_post_signifyd(self): def _should_post_signifyd(self):
# If we have no transaction/acquirer we will still send! # If we have no transaction/acquirer we will still send!
# this case is useful for admin or free orders but could be customized here. # this case is useful for admin or free orders but could be customized here.
# case_required = bool(self.website_id.signifyd_connector_id.signifyd_case_type) acquirers = self.transaction_ids.acquirer_id
# a_case_types = self.transaction_ids.mapped('acquirer_id.signifyd_case_type') if acquirers and not any(acquirers.mapped('signifyd_case_required')):
# if a_case_types: return False
# case_required = any(a_case_types) return True
# return self.state in ('sale', 'done') and not self.signifyd_case_id and case_required
case_required = bool(self.website_id.signifyd_connector_id.signifyd_case_type)
case_required = self.website_id.signifyd_connector_id.signifyd_case_type not in [
self.env.ref('website_sale_signifyd.signifyd_coverage_none').id,
False
]
coverage_types = self.transaction_ids.signifyd_coverage_ids
def post_signifyd_case(self): def post_signifyd_case(self):
if not self.website_id.signifyd_connector_id: if not self.website_id.signifyd_connector_id:
@@ -78,8 +69,26 @@ class SaleOrder(models.Model):
# TODO do we need to raise an exception? # TODO do we need to raise an exception?
return None return None
def _get_coverage_types(self):
coverage_none = self.env.ref('website_sale_signifyd.signifyd_coverage_none')
coverage_all = self.env.ref('website_sale_signifyd.signifyd_coverage_all')
acquirer_coverage_types = self.transaction_ids.acquirer_id.signifyd_coverage_ids
# 'ALL' if specified by any acquirer
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):
return coverage_none
# Specific acquirer-level coverage types
if acquirer_coverage_types - coverage_none:
return acquirer_coverage_types - coverage_none
# Default: connector-level
return self.website_id.signifyd_connector_id.signifyd_coverage_ids or coverage_none
@api.model @api.model
def _prepare_signifyd_case_values(self, order_session_id, checkout_token, browser_ip_address): def _prepare_signifyd_case_values(self, order_session_id, checkout_token, browser_ip_address):
coverage_codes = self._get_coverage_types().mapped('code')
decision_request = self.website_id.signifyd_connector_id.signifyd_case_type or 'DECISION' decision_request = self.website_id.signifyd_connector_id.signifyd_case_type or 'DECISION'
# find the highest 'acquirer override' # find the highest 'acquirer override'
@@ -102,6 +111,7 @@ class SaleOrder(models.Model):
} }
recipients = self.partner_invoice_id + self.partner_shipping_id recipients = self.partner_invoice_id + self.partner_shipping_id
# API v3 WIP
new_case_vals = { new_case_vals = {
# FIXME: UUID? # FIXME: UUID?
'orderId': self.id, 'orderId': self.id,
@@ -132,7 +142,7 @@ 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' 'coverageRequests': coverage_codes,
} }
@@ -142,82 +152,83 @@ class SaleOrder(models.Model):
if not line[key]: if not line[key]:
line.pop(key) line.pop(key)
new_case_vals = { # API v2
'decisionRequest': { # new_case_vals = {
'paymentFraud': decision_request, # 'decisionRequest': {
}, # 'paymentFraud': decision_request,
'purchase': { # },
"orderSessionId": order_session_id, # 'purchase': {
"orderId": self.id, # "orderSessionId": order_session_id,
"checkoutToken": checkout_token, # "orderId": self.id,
"browserIpAddress": browser_ip_address, # "checkoutToken": checkout_token,
"currency": self.partner_id.currency_id.name, # "browserIpAddress": browser_ip_address,
"orderChannel": "WEB", # "currency": self.partner_id.currency_id.name,
"totalPrice": self.amount_total, # "orderChannel": "WEB",
'products': [ # "totalPrice": self.amount_total,
{ # 'products': [
"itemId": line.product_id.id, # {
"itemName": line.product_id.name, # "itemId": line.product_id.id,
"itemIsDigital": False, # "itemName": line.product_id.name,
"itemCategory": line.product_id.categ_id.name, # "itemIsDigital": False,
"itemUrl": line.product_id.website_url or '', # "itemCategory": line.product_id.categ_id.name,
"itemQuantity": line.product_uom_qty, # "itemUrl": line.product_id.website_url or '',
"itemPrice": line.price_unit, # "itemQuantity": line.product_uom_qty,
"itemWeight": line.product_id.weight or 0.1, # "itemPrice": line.price_unit,
} # "itemWeight": line.product_id.weight or 0.1,
for line in self.order_line if line.product_id # }
], # for line in self.order_line if line.product_id
'shipments': [{ # ],
"shipper": carrier.name, # 'shipments': [{
"shippingMethod": "ground", # "shipper": carrier.name,
"shippingPrice": self.amount_delivery, # "shippingMethod": "ground",
} # "shippingPrice": self.amount_delivery,
for carrier in self.carrier_id # }
], # for carrier in self.carrier_id
}, # ],
'recipients': [ # },
{ # 'recipients': [
"fullName": partner.name, # {
"confirmationEmail": partner.email, # "fullName": partner.name,
"confirmationPhone": partner.phone, # "confirmationEmail": partner.email,
"organization": partner.company_id.name, # "confirmationPhone": partner.phone,
"deliveryAddress": { # "organization": partner.company_id.name,
"streetAddress": partner.street, # "deliveryAddress": {
"unit": partner.street2, # "streetAddress": partner.street,
"city": partner.city, # "unit": partner.street2,
"provinceCode": partner.state_id.code, # "city": partner.city,
"postalCode": partner.zip, # "provinceCode": partner.state_id.code,
"countryCode": partner.country_id.code, # "postalCode": partner.zip,
} # "countryCode": partner.country_id.code,
} # }
for partner in recipients # }
], # for partner in recipients
'transactions': [ # ],
{ # 'transactions': [
"parentTransactionId": None, # {
"transactionId": tx.id, # "parentTransactionId": None,
"gateway": tx.acquirer_id.name, # "transactionId": tx.id,
"paymentMethod": "CREDIT_CARD", # "gateway": tx.acquirer_id.name,
"gatewayStatusCode": tx_status_type.get(tx.state, 'PENDING'), # "paymentMethod": "CREDIT_CARD",
"type": "AUTHORIZATION", # "gatewayStatusCode": tx_status_type.get(tx.state, 'PENDING'),
"currency": self.partner_id.currency_id.name, # "type": "AUTHORIZATION",
"amount": tx.amount, # "currency": self.partner_id.currency_id.name,
# "avsResponseCode": "Y", # "amount": tx.amount,
# "cvvResponseCode": "N", # # "avsResponseCode": "Y",
"checkoutPaymentDetails": { # # "cvvResponseCode": "N",
"holderName": tx.partner_id.name, # "checkoutPaymentDetails": {
"billingAddress": { # "holderName": tx.partner_id.name,
"streetAddress": tx.partner_id.street, # "billingAddress": {
"unit": tx.partner_id.street2, # "streetAddress": tx.partner_id.street,
"city": tx.partner_id.city, # "unit": tx.partner_id.street2,
"provinceCode": tx.partner_id.state_id.code, # "city": tx.partner_id.city,
"postalCode": tx.partner_id.zip, # "provinceCode": tx.partner_id.state_id.code,
"countryCode": tx.partner_id.country_id.code, # "postalCode": tx.partner_id.zip,
} # "countryCode": tx.partner_id.country_id.code,
} # }
} # }
for tx in self.transaction_ids # }
], # for tx in self.transaction_ids
} # ],
# }
return new_case_vals return new_case_vals

View File

@@ -27,7 +27,12 @@ class SignifydConnector(models.Model):
('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.')
@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 # TODO ideally this would be a regular constant
# however other entities currently use this by reference # however other entities currently use this by reference

View File

@@ -8,3 +8,7 @@ class SignifydCoverage(models.Model):
name = fields.Char(required=True) name = fields.Char(required=True)
description = fields.Char() description = fields.Char()
code = fields.Char(required=True) code = fields.Char(required=True)
exclusive = fields.Boolean())
def _apply_exclusivity(self):
return self.filtered('exclusive')[:1] or self