Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions payment_worldline/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Worldline

## Technical details

API:
[Worldline Direct API](https://docs.direct.worldline-solutions.com/en/api-reference)
version `2`

This module integrates Worldline using the generic payment with redirection flow based
on form submission provided by the `payment` module.

This is achieved by following the [Hosted Checkout Page]
(https://docs.direct.worldline-solutions.com/en/integration/basic-integration-methods/hosted-checkout-page)
guide.

## Supported features

- Payment with redirection flow
- Webhook notifications
- Tokenization with payment

## Not implemented features

- Tokenization without payment
- Manual capture
- Refunds

## Module history

- `18.0`
- The first version of the module is merged. odoo/odoo#175194.
- `16.0`
- bACKPORT TO 16.0.

## Testing instructions

https://docs.direct.worldline-solutions.com/en/integration/how-to-integrate/test-cases/index

Use any name, any date in the future, and any 3 or 4 digits CVC.

### VISA

**Card Number**: `4330264936344675`

### 3D Secure 2 (VISA)

**Card Number**: `4874970686672022`
14 changes: 14 additions & 0 deletions payment_worldline/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import controllers
from . import models

from odoo.addons.payment import setup_provider, reset_payment_provider


def post_init_hook(cr, registry):
setup_provider(cr, registry, "worldline")


def uninstall_hook(cr, registry):
reset_payment_provider(cr, registry, "worldline")
22 changes: 22 additions & 0 deletions payment_worldline/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

{
"name": "Payment Provider: Worldline",
"category": "Accounting/Payment Providers",
"sequence": 350,
"version": "16.0.1.0.0",
"summary": "A French payment provider covering several European countries.",
"author": "Odoo SA, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/account-payment",
"depends": ["payment"],
"data": [
"views/payment_icon.xml",
"views/payment_provider_views.xml",
"views/payment_worldline_templates.xml",
"data/payment_icon.xml",
"data/payment_provider_data.xml",
],
"post_init_hook": "post_init_hook",
"uninstall_hook": "uninstall_hook",
"license": "LGPL-3",
}
78 changes: 78 additions & 0 deletions payment_worldline/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

# The codes of the payment methods to activate when Worldline is activated.
DEFAULT_PAYMENT_METHOD_CODES = {
# Primary payment methods.
"card",
}

# Mapping of payment method codes to Worldline codes.
# See https://docs.direct.worldline-solutions.com/en/payment-methods-and-features/index.
PAYMENT_METHODS_MAPPING = {
"alipay_plus": 5405,
"amex": 2,
"bancontact": 3012,
"bizum": 5001,
"cartes_bancaires": 130,
"cofidis": 3012,
"diners": 132,
"discover": 128,
"eps": 5406,
"floa_bank": 5139,
"ideal": 809,
"jcb": 125,
"klarna": 3301,
"maestro": 117,
"mastercard": 3,
"mbway": 5908,
"multibanco": 5500,
"p24": 3124,
"paypal": 840,
"post_finance_pay": 3203,
"twint": 5407,
"upi": 56,
"visa": 1,
"wechat_pay": 5404,
}

# The payment methods that involve a redirection to 3rd parties by Worldline.
REDIRECT_PAYMENT_METHODS = {
"alipay_plus",
"bizum",
"eps",
"floa_bank",
"ideal",
"klarna",
"mbway",
"multibanco",
"p24",
"paypal",
"post_finance_pay",
"twint",
"wechat_pay",
}

# Mapping of transaction states to Worldline's payment statuses.
# See https://docs.direct.worldline-solutions.com/en/integration/api-developer-guide/statuses.
PAYMENT_STATUS_MAPPING = {
"pending": (
"CREATED",
"REDIRECTED",
"AUTHORIZATION_REQUESTED",
"PENDING_CAPTURE",
"CAPTURE_REQUESTED",
),
"done": ("CAPTURED",),
"cancel": ("CANCELLED",),
"declined": ("REJECTED", "REJECTED_CAPTURE"),
}

# Mapping of response codes indicating Worldline handled the request
# See
# https://apireference.connect.worldline-solutions.com/s2sapi
# /v1/en_US/json/response-codes.html.
VALID_RESPONSE_CODES = {
200: "Successful",
201: "Created",
402: "Payment Rejected",
}
3 changes: 3 additions & 0 deletions payment_worldline/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import main
121 changes: 121 additions & 0 deletions payment_worldline/controllers/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

import hashlib
import hmac
import logging
import pprint
from base64 import b64encode

from werkzeug.exceptions import Forbidden

from odoo import http
from odoo.exceptions import ValidationError
from odoo.http import request

_logger = logging.getLogger(__name__)


class WorldlineController(http.Controller):
_return_url = "/payment/worldline/return"
_webhook_url = "/payment/worldline/webhook"

@http.route(_return_url, type="http", auth="public", methods=["GET"])
def worldline_return_from_checkout(self, **data):
"""Process the notification data sent by Worldline after redirection.

:param dict data: The notification data, including the provider
id appended to the URL in
`_get_specific_rendering_values`.
"""
_logger.info(
"Handling redirection from Worldline with data:\n%s", pprint.pformat(data)
)

provider_id = int(data["provider_id"])
provider_sudo = (
request.env["payment.provider"].sudo().browse(provider_id).exists()
)
if not provider_sudo or provider_sudo.code != "worldline":
_logger.warning("Received payment data with invalid provider id.")
raise Forbidden()

# Fetch the checkout session data from Worldline.
checkout_session_data = provider_sudo._worldline_make_request(
f'hostedcheckouts/{data["hostedCheckoutId"]}', method="GET"
)
_logger.info(
"Response of '/hostedcheckouts/<hostedCheckoutId>' request:\n%s",
pprint.pformat(checkout_session_data),
)
notification_data = checkout_session_data.get("createdPaymentOutput", {})

# Handle the notification data.
tx_sudo = (
request.env["payment.transaction"]
.sudo()
._get_tx_from_notification_data("worldline", notification_data)
)
tx_sudo._handle_notification_data("worldline", notification_data)
return request.redirect("/payment/status")

@http.route(_webhook_url, type="http", auth="public", methods=["POST"], csrf=False)
def worldline_webhook(self):
"""Process the notification data sent by Worldline to the webhook.

See https://docs.direct.worldline-solutions.com/en
/integration/api-developer-guide/webhooks.

:return: An empty string to acknowledge the notification.
:rtype: str
"""
notification_data = request.get_json_data()
_logger.info(
"Notification received from Worldline with data:\n%s",
pprint.pformat(notification_data),
)
try:
# Check the integrity of the notification.
tx_sudo = (
request.env["payment.transaction"]
.sudo()
._get_tx_from_notification_data("worldline", notification_data)
)
received_signature = request.httprequest.headers.get("X-GCS-Signature")
request_data = request.httprequest.data
self._verify_notification_signature(
request_data, received_signature, tx_sudo
)

# Handle the notification data.
tx_sudo._handle_notification_data("worldline", notification_data)
except ValidationError: # Acknowledge the notification to avoid getting spammed.
_logger.exception(
"Unable to handle the notification data; skipping to acknowledge."
)

return request.make_json_response("") # Acknowledge the notification.

@staticmethod
def _verify_notification_signature(request_data, received_signature, tx_sudo):
"""Check that the received signature matches the expected one.

:param dict|bytes request_data: The request data.
:param str received_signature: The signature to compare with the expected signature.
:param payment.transaction tx_sudo: The sudoed transaction
referenced by the notification data.
:return: None
:raise Forbidden: If the signatures don't match.
"""
# Retrieve the received signature from the payload.
if not received_signature:
_logger.warning("Received notification with missing signature.")
raise Forbidden()

# Compare the received signature with the expected signature computed from the payload.
webhook_secret = tx_sudo.provider_id.worldline_webhook_secret
expected_signature = b64encode(
hmac.new(webhook_secret.encode(), request_data, hashlib.sha256).digest()
)
if not hmac.compare_digest(received_signature.encode(), expected_signature):
_logger.warning("Received notification with invalid signature.")
raise Forbidden()
7 changes: 7 additions & 0 deletions payment_worldline/data/neutralize.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- disable worldline payment provider
UPDATE payment_provider
SET worldline_pspid = NULL,
worldline_api_key = NULL,
worldline_api_secret = NULL,
worldline_webhook_key = NULL,
worldline_webhook_secret = NULL;
52 changes: 52 additions & 0 deletions payment_worldline/data/payment_icon.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>

<record id="payment.payment_icon_cc_visa" model="payment.icon">
<field name="worldline_code">visa</field>
</record>

<record id="payment.payment_icon_cc_mastercard" model="payment.icon">
<field name="worldline_code">mastercard</field>
</record>

<record id="payment.payment_icon_cc_american_express" model="payment.icon">
<field name="worldline_code">amex</field>
</record>

<record id="payment.payment_icon_cc_discover" model="payment.icon">
<field name="worldline_code">discover</field>
</record>

<record id="payment.payment_icon_cc_diners_club_intl" model="payment.icon">
<field name="worldline_code">diners</field>
</record>

<record id="payment.payment_icon_paypal" model="payment.icon">
<field name="worldline_code">paypal</field>
</record>

<record id="payment.payment_icon_cc_jcb" model="payment.icon">
<field name="worldline_code">jcb</field>
</record>

<record id="payment.payment_icon_cc_maestro" model="payment.icon">
<field name="worldline_code">maestro</field>
</record>

<record id="payment.payment_icon_cc_bancontact" model="payment.icon">
<field name="worldline_code">bancontact</field>
</record>

<record id="payment.payment_icon_cc_ideal" model="payment.icon">
<field name="worldline_code">ideal</field>
</record>

<record id="payment.payment_icon_cc_eps" model="payment.icon">
<field name="worldline_code">eps</field>
</record>

<record id="payment.payment_icon_cc_p24" model="payment.icon">
<field name="worldline_code">p24</field>
</record>

</odoo>
32 changes: 32 additions & 0 deletions payment_worldline/data/payment_provider_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">

<record id="payment_provider_worldline" model="payment.provider">
<field name="code">worldline</field>
<field name="redirect_form_view_id" ref="redirect_form" />
<field name="allow_tokenization">True</field>
<field name="name">Wordline</field>
<field name="display_as">Wordline</field>
<field
name="image_128"
type="base64"
file="payment_worldline/static/description/icon.png"
/>
<field name="module_id" ref="base.module_payment_worldline" />
<field
name="payment_icon_ids"
eval="[Command.set([
ref('payment.payment_icon_cc_maestro'),
ref('payment.payment_icon_cc_mastercard'),
ref('payment.payment_icon_cc_discover'),
ref('payment.payment_icon_cc_diners_club_intl'),
ref('payment.payment_icon_cc_jcb'),
ref('payment.payment_icon_cc_american_express'),
ref('payment.payment_icon_cc_bancontact'),
ref('payment.payment_icon_cc_unionpay'),
ref('payment.payment_icon_cc_visa'),
])]"
/>
</record>

</odoo>
Loading