Skip to content

Commit c1d6b8b

Browse files
[BOT] Migrate release 4.2.0 to GitHub.
1 parent aa015b2 commit c1d6b8b

25 files changed

+2697
-1136
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
4.2.0, 2025-07-25
2+
=============
3+
- Embedded payment fields integration.
4+
15
4.1.0, 2025-04-03
26
=============
37
- Compatibility with Odoo 18.

payment_lyra/__manifest__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
{
1111
'name': 'Lyra Collect Payment Provider',
12-
'version': '4.1.0',
12+
'version': '4.2.0',
1313
'summary': 'Accept payments with Lyra Collect secure payment gateway.',
1414
'category': 'Accounting/Payment Providers',
1515
'author': 'Lyra Network',
@@ -23,6 +23,11 @@
2323
'data/payment_provider_data.xml',
2424
'security/ir.model.access.csv',
2525
],
26+
'assets': {
27+
'web.assets_frontend': [
28+
'payment_lyra/static/src/**/*'
29+
]
30+
},
2631
'post_init_hook': 'post_init_hook',
2732
'uninstall_hook': 'uninstall_hook',
2833
'images': ['static/description/icon.png'],

payment_lyra/controllers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88
# License: http://www.gnu.org/licenses/agpl.html GNU Affero General Public License (AGPL v3)
99

1010
from . import main
11+
from . import rest

payment_lyra/controllers/main.py

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,10 @@
1010
import logging
1111
import pprint
1212

13-
from pkg_resources import parse_version
14-
import werkzeug
15-
16-
from odoo import http, release
13+
from odoo import http
1714
from odoo.http import request
1815
from odoo.exceptions import ValidationError
16+
from ..helpers import tools
1917

2018
_logger = logging.getLogger(__name__)
2119

@@ -40,11 +38,29 @@ def lyra_return_from_checkout(self, **pdt_data):
4038
_logger.info('Lyra Collect: entering _from_notification with data %s', pprint.pformat(pdt_data))
4139

4240
try:
43-
# Check the origin of the notification
44-
tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data('lyra', pdt_data)
41+
is_rest = False
42+
data = pdt_data
43+
44+
# Check the type of integration.
45+
if tools.check_rest_response(pdt_data):
46+
data = tools.convert_rest_result(pdt_data)
47+
data['is_rest'] = '1'
48+
is_rest = True
49+
50+
tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data('lyra', data)
4551

46-
# Handle the notification data
47-
tx_sudo._handle_notification_data('lyra', pdt_data)
52+
# Verify hash.
53+
if is_rest:
54+
hmac256_key = tx_sudo.provider_id._lyra_get_rest_sha256_key()
55+
hash_checked = tools.check_hash(pdt_data, hmac256_key)
56+
if not hash_checked:
57+
error_msg = 'Lyra Collect: invalid signature for data {}'.format(pdt_data)
58+
_logger.info(error_msg)
59+
60+
raise ValidationError(error_msg)
61+
62+
# Handle the notification data.
63+
tx_sudo._handle_notification_data('lyra', data)
4864
except ValidationError:
4965
_logger.exception("Lyra Collect: Unable to handle the return notification data; skipping to acknowledge.")
5066

@@ -58,12 +74,36 @@ def lyra_ipn(self, **post):
5874
_logger.info('Lyra Collect: entering IPN _get_tx_from_notification with post data %s', pprint.pformat(post))
5975

6076
try:
61-
result = request.env['payment.transaction'].sudo()._get_tx_from_notification_data('lyra', post)
77+
is_rest = False
78+
data = post
79+
80+
# Check the type of integration.
81+
if tools.check_rest_response(post):
82+
if not tools.order_cycle_closed(post):
83+
return 'Payment failure.'
84+
85+
data = tools.convert_rest_result(post)
86+
data['is_rest'] = '1'
87+
is_rest = True
88+
89+
result = request.env['payment.transaction'].sudo()._get_tx_from_notification_data('lyra', data)
90+
91+
if is_rest:
92+
rest_password = result.provider_id._lyra_get_rest_password()
93+
hash_checked = tools.check_hash(post, rest_password)
94+
if not hash_checked:
95+
error_msg = 'Lyra Collect: invalid signature for data {}'.format(post)
96+
_logger.info(error_msg)
97+
98+
raise ValidationError(error_msg)
99+
100+
if (data.get('vads_trans_status') == 'ABANDONED') or (data.get('vads_trans_status') == 'CANCELED') and (data.get('vads_order_status') == 'UNPAID') and (data.get('vads_order_cycle') == 'CLOSED'):
101+
return 'Payment abandoned.'
62102

63-
# Handle the notification data
64-
result._handle_notification_data('lyra', post)
65-
except ValidationError: #Acknowledge the notification to avoid getting spammed
103+
# Handle the notification data.
104+
result._handle_notification_data('lyra', data)
105+
except ValidationError: # Acknowledge the notification to avoid getting spammed.
66106
_logger.exception("Lyra Collect: Unable to handle the IPN notification data; skipping to acknowledge.")
67107
return 'Bad request received.'
68108

69-
return 'Accepted payment, order has been updated.' if result else 'Payment failure, order has been cancelled.'
109+
return 'Payment processed, order has been updated.' if result else 'An error occurred while processing payment.'

payment_lyra/controllers/rest.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# coding: utf-8
2+
#
3+
# Copyright © Lyra Network.
4+
# This file is part of Lyra Collect plugin for Odoo. See COPYING.md for license details.
5+
#
6+
# Author: Lyra Network (https://www.lyra.com)
7+
# Copyright: Copyright © Lyra Network
8+
# License: http://www.gnu.org/licenses/agpl.html GNU Affero General Public License (AGPL v3)
9+
10+
import logging
11+
import base64
12+
import requests
13+
import json
14+
15+
from odoo.tools import float_round
16+
from odoo import http
17+
from odoo.http import request
18+
19+
from ..helpers import tools, constants
20+
_logger = logging.getLogger(__name__)
21+
22+
class LyraRestController(http.Controller):
23+
@http.route("/payment/lyra/createFormToken", type="http", auth='public', methods=['POST'], csrf=False)
24+
def lyra_refresh_form_token(self, **post):
25+
processing_values = json.loads(request.httprequest.data.decode('utf-8'))
26+
provider_id = processing_values["provider_id"]
27+
payment_provider = request.env['payment.provider'].sudo().browse(provider_id).exists()
28+
29+
# On payment method selection, we have only order ID.
30+
if "order_id" in processing_values:
31+
processed_values = payment_provider.lyra_generate_values_from_order(processing_values)
32+
else:
33+
# On payment submit, we have transaction data.
34+
payment_transaction = request.env['payment.transaction'].sudo().search([('reference', '=', processing_values["reference"])]).exists()
35+
36+
sale_order = payment_transaction.sale_order_ids[0]
37+
sale_order._check_cart_is_ready_to_be_paid()
38+
39+
# Check amount coherence.
40+
compare_amounts = sale_order.currency_id.compare_amounts
41+
if (compare_amounts(float(processing_values['amount']), sale_order.amount_total)):
42+
return json.dumps({ "formToken": "NO_UPDATE" })
43+
44+
processed_values = payment_transaction._get_specific_rendering_values(processing_values)
45+
processed_values["vads_order_id"] = processing_values["reference"].rpartition('-')[0]
46+
47+
currency = payment_provider._lyra_get_currency(processing_values["currency_id"])[0]
48+
49+
params = self.generate_form_token_data(processed_values, payment_provider, currency)
50+
form_token = self.lyra_create_form_token(params, payment_provider)
51+
return json.dumps({ "formToken": form_token })
52+
53+
def generate_form_token_data(self, values, payment_provider, currency):
54+
params = {
55+
"amount": values["vads_amount"],
56+
"currency": currency,
57+
"orderId": values["vads_order_id"],
58+
"customer": {
59+
"email": values["vads_cust_email"],
60+
"reference": values["vads_cust_id"],
61+
"billingDetails": {
62+
"firstName": values["vads_cust_first_name"],
63+
"lastName": values["vads_cust_last_name"],
64+
"address": values["vads_cust_address"],
65+
"zipCode": values["vads_cust_zip"],
66+
"state": values["vads_cust_state"],
67+
"city": values["vads_cust_city"],
68+
"phoneNumber": values["vads_cust_phone"],
69+
"country": values["vads_cust_country"],
70+
"language": values["vads_language"],
71+
}
72+
},
73+
"shipingDetails": {
74+
"firstName": values["vads_ship_to_first_name"],
75+
"lastName": values["vads_ship_to_last_name"],
76+
"address": values["vads_ship_to_street"],
77+
"zipCode": values["vads_ship_to_zip"],
78+
"city": values["vads_ship_to_city"],
79+
"state": values["vads_ship_to_state"],
80+
"phoneNumber": values["vads_ship_to_phone_num"],
81+
"country": values["vads_ship_to_country"],
82+
},
83+
"transactionOptions": {
84+
"cardOptions": {
85+
"paymentSource": "EC"
86+
}
87+
},
88+
"contrib": values["vads_contrib"],
89+
}
90+
91+
validation_mode = payment_provider.lyra_validation_mode
92+
if validation_mode == "1":
93+
params["transactionOptions"]["cardOptions"]["manualValidation"] = "YES"
94+
elif validation_mode == "0":
95+
params["transactionOptions"]["cardOptions"]["manualValidation"] = "NO"
96+
97+
capture_delay = payment_provider.lyra_capture_delay
98+
if capture_delay and capture_delay.isdigit():
99+
params["transactionOptions"]["cardOptions"]["captureDelay"] = capture_delay
100+
101+
retry = payment_provider.lyra_embedded_payment_attempts
102+
if retry and retry.isdigit():
103+
params["transactionOptions"]["cardOptions"]["retry"] = retry
104+
105+
cards = payment_provider._lyra_get_embedded_payment_means()
106+
if cards != []:
107+
params['paymentMethods'] = tuple(cards)
108+
109+
return params
110+
111+
def lyra_create_form_token(self, values, payment_provider):
112+
try:
113+
identification = payment_provider.lyra_site_id + ":" + payment_provider._lyra_get_rest_password()
114+
headers = {
115+
"Authorization": "Basic " + base64.b64encode((identification.encode('utf-8'))).decode('utf-8'),
116+
"Content-Type": "application/json"
117+
}
118+
119+
request_url = constants.LYRA_PARAMS.get('REST_URL') + 'V4/Charge/CreatePayment'
120+
121+
response = requests.post(url = request_url, json = values, headers = headers)
122+
if response.json().get("status") != "SUCCESS":
123+
_logger.error("Error while creating form token: " + response.json().get("answer", {}).get("errorMessage") + "(" + response.json().get("answer", {}).get("errorCode") + ").")
124+
if "detailedErrorMessage" in response.json().get("answer", {}) and response.json().get("answer", {}).get("detailedErrorMessage") is not None:
125+
_logger.error("Detailed message: " + response.json().get("answer", {}).get("detailedErrorMessage") + "(" + response.json().get("answer", {}).get("detailedErrorCode") + ").")
126+
else:
127+
msg = ""
128+
if "orderId" in values:
129+
msg = " for order #" + values['orderId']
130+
131+
_logger.info("Form token created successfully {} with data: {}".format(msg, values))
132+
133+
return response.json().get("answer", {}).get("formToken")
134+
except Exception as exc:
135+
_logger.error(exc)
136+
137+
return False

payment_lyra/data/payment_provider_data.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<field name="state">test</field>
1919
<field name="company_id" ref="base.main_company" />
2020
<field name="redirect_form_view_id" ref="lyra_provider_button" />
21+
<field name="inline_form_view_id" ref="lyra_inline_embedded" />
2122
<field name="environment">test</field>
2223
<field name="pre_msg"><![CDATA[<p>You will be redirected to the Lyra Collect website after clicking on the payment button.</p>]]></field>
2324

@@ -38,5 +39,6 @@
3839

3940
<function model="payment.provider" name="multi_add">
4041
<value>/data/payment_provider_data_multi.xml</value>
42+
<value>True</value>
4143
</function>
4244
</odoo>

payment_lyra/helpers/constants.py

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
# Copyright: Copyright © Lyra Network
88
# License: http://www.gnu.org/licenses/agpl.html GNU Affero General Public License (AGPL v3)
99

10-
from odoo import _
10+
from odoo.tools.translate import _lt
1111

1212
# WARN: Do not modify code format here. This is managed by build files.
1313
LYRA_PLUGIN_FEATURES = {
@@ -21,7 +21,7 @@
2121
'GATEWAY_CODE': 'Lyra',
2222
'GATEWAY_NAME': 'Lyra Collect',
2323
'BACKOFFICE_NAME': 'Lyra Expert',
24-
'SUPPORT_EMAIL': 'support-ecommerce@lyra-collect.com',
24+
'SUPPORT_EMAIL': 'https://support.lyra.com/hc/fr/requests/new',
2525
'GATEWAY_URL': 'https://secure.lyra.com/vads-payment/',
2626
'SITE_ID': '12345678',
2727
'KEY_TEST': '1111111111111111',
@@ -31,24 +31,26 @@
3131
'LANGUAGE': 'en',
3232

3333
'GATEWAY_VERSION': 'V2',
34-
'PLUGIN_VERSION': '4.1.0',
34+
'PLUGIN_VERSION': '4.2.0',
3535
'CMS_IDENTIFIER': 'Odoo_17-18',
36+
'REST_URL': 'https://api.lyra.com/api-payment/',
37+
'STATIC_URL': 'https://static.lyra.com/static/'
3638
}
3739

3840
LYRA_LANGUAGES = {
39-
'cn': 'Chinese',
40-
'de': 'German',
41-
'es': 'Spanish',
42-
'en': 'English',
43-
'fr': 'French',
44-
'it': 'Italian',
45-
'jp': 'Japanese',
46-
'nl': 'Dutch',
47-
'pl': 'Polish',
48-
'pt': 'Portuguese',
49-
'ru': 'Russian',
50-
'sv': 'Swedish',
51-
'tr': 'Turkish',
41+
'cn': _lt("Chinese"),
42+
'de': _lt("German"),
43+
'es': _lt("Spanish"),
44+
'en': _lt("English"),
45+
'fr': _lt("French"),
46+
'it': _lt("Italian"),
47+
'jp': _lt("Japanese"),
48+
'nl': _lt("Dutch"),
49+
'pl': _lt("Polish"),
50+
'pt': _lt("Portuguese"),
51+
'ru': _lt("Russian"),
52+
'sv': _lt("Swedish"),
53+
'tr': _lt("Turkish"),
5254
}
5355

5456
LYRA_CARDS = {
@@ -62,7 +64,7 @@
6264
'AMEX': u'American Express',
6365
'ACCORD_STORE': u'Cartes Enseignes Partenaires',
6466
'ACCORD_STORE_SB': u'Cartes Enseignes Partenaires (sandbox)',
65-
'AKULAKU_ID': u'Akulaku PayLater',
67+
'AKULAKU_ID': u'Akulaku PayLater ID',
6668
'AKULAKU_PH': u'Akulaku PayLater PH',
6769
'ALINEA': u'Carte myalinea',
6870
'ALINEA_CDX': u'Carte Cadeau Alinéa',
@@ -236,4 +238,13 @@
236238
'es': 'Español',
237239
'de': 'Deutsch',
238240
'pt': 'Português',
239-
}
241+
}
242+
243+
LYRA_PAYMENT_DATA_ENTRY_MODE = {
244+
'redirect': _lt("Bank data acquisition on payment gateway"),
245+
'embedded': _lt("Embedded payment fields on merchant site (REST API)"),
246+
'embedded_extended_with_logos': _lt("Embedded payment fields extended on merchant site with logos (REST API)"),
247+
'embedded_extended_without_logos': _lt("Embedded payment fields extended on merchant site without logos (REST API)"),
248+
}
249+
250+
LYRA_REST_API_KEYS_DESC = 'REST API keys are available in your Lyra Expert Back Office (menu: Settings > Shops > REST API keys).'

0 commit comments

Comments
 (0)