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
6 changes: 6 additions & 0 deletions setup/shopinvader_api_payment_provider_worldline/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import setuptools

setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)
Empty file.
2 changes: 2 additions & 0 deletions shopinvader_api_payment_provider_worldline/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import models
from . import routers
19 changes: 19 additions & 0 deletions shopinvader_api_payment_provider_worldline/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright 2025 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

{
"name": "Shopinvader Api Payment Provider Wordline",
"summary": """
Specific routes for Worldline payments from Shopinvader""",
"version": "16.0.1.0.0",
"license": "AGPL-3",
"author": "ACSONE SA/NV,Shopinvader",
"website": "https://github.com/shopinvader/odoo-shopinvader-payment",
"depends": [
"fastapi",
"shopinvader_api_payment",
"payment_worldline",
],
"data": [],
"demo": [],
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import payment_transaction
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Copyright Odoo SA (https://odoo.com)
# Copyright 2025 ACSONE SA (https://acsone.eu).
# License LGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from urllib.parse import urljoin

from werkzeug import urls

from odoo import models


class PaymentTransaction(models.Model):
_inherit = "payment.transaction"

def _worldline_create_checkout_session_get_payload(self):
self.ensure_one()
payload = super()._worldline_create_checkout_session_get_payload()
if not self.env.context.get("shopinvader_api_payment"):
return payload
shopinvader_api_base_url = self.shopinvader_frontend_redirect_url
return_url = urljoin(
shopinvader_api_base_url, "/shopinvader/payment/providers/worldline/return"
)
return_url_params = urls.url_encode({"provider_id": str(self.provider_id.id)})
payload["hostedCheckoutSpecificInput"][
"returnUrl"
] = f"{return_url}?{return_url_params}"
return payload
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Benjamin Willig <[email protected]>
6 changes: 6 additions & 0 deletions shopinvader_api_payment_provider_worldline/readme/USAGE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
**Specific procedure for Worldline**

First call the POST `/payment/transactions` as described in the core addon `shopinvader_api_payment`.
This method will return a `TransactionProcessingValues` schema in which you will find a `redirect_form_html` HTML form.
This HTML form must just be submitted by the front to call the Worldline services.
After worldline has finished, the frontend redirect url will be called back.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import payment_worldline
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Copyright Odoo SA (https://odoo.com)
# Copyright 2025 ACSONE SA (https://acsone.eu).
# License LGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

import logging
import pprint
from typing import Annotated
from urllib.parse import quote_plus

from fastapi import Depends, Request
from fastapi.responses import RedirectResponse
from werkzeug.exceptions import Forbidden

from odoo import _, api, models
from odoo.exceptions import ValidationError

from odoo.addons.fastapi.dependencies import odoo_env
from odoo.addons.payment_worldline.controllers.main import WorldlineController
from odoo.addons.shopinvader_api_payment.routers import payment_router
from odoo.addons.shopinvader_api_payment.routers.utils import (
add_query_params_in_url,
tx_state_to_redirect_status,
)

_logger = logging.getLogger(__name__)


@payment_router.get("/payment/providers/worldline/return")
async def worldline_return(
request: Request,
odoo_env: Annotated[api.Environment, Depends(odoo_env)],
) -> RedirectResponse:
"""Handle SIPS return.

After the payment, the user is redirected with a POST to this endpoint. We handle
the notification data to update the transaction status. We then redirect the browser
with a GET to the frontend_return_url, with the transaction reference as parameter.

Future: we could also return a unguessable transaction uuid that the front could the
use to consult /payment/transactions/{uuid} and obtain the transaction status.
"""
data = await request.form()
_logger.info(
"return notification received from Worldline with data:\n%s",
pprint.pformat(data),
)
params = request.query_params
hosted_checkout_id = params.get("hostedCheckoutId")
provider_id = int(params.get("provider_id", 0))

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

checkout_session_data = provider._worldline_make_request(
f"hostedcheckouts/{hosted_checkout_id}", method="GET"
)
_logger.info(
"Response of '/hostedcheckouts/<hostedCheckoutId>' request:\n%s",
pprint.pformat(checkout_session_data),
)
notification_data = checkout_session_data.get("createdPaymentOutput", {})

tx_sudo = (
odoo_env["payment.transaction"]
.sudo()
._get_tx_from_notification_data("worldline", notification_data)
)
reference = tx_sudo.display_name
frontend_redirect_url = tx_sudo.shopinvader_frontend_redirect_url
try:
tx_sudo._handle_notification_data("worldline", notification_data)
status = tx_state_to_redirect_status(tx_sudo.state)
except Exception:
_logger.exception("unable to handle worldline notification data", exc_info=True)
status = "error"
return RedirectResponse(
url=add_query_params_in_url(
frontend_redirect_url,
{"status": status, "reference": quote_plus(reference)},
),
status_code=303,
)


@payment_router.post("/payment/providers/worldline/webhook")
async def worldline_webhook(
request: Request,
odoo_env: Annotated[api.Environment, Depends(odoo_env)],
):
"""Handle Wordline webhook."""
data = await request.json()
_logger.info(
"webhook notification received from SIPS with data:\n%s", pprint.pformat(data)
)
try:
tx_sudo = (
odoo_env["payment.transaction"]
.sudo()
._get_tx_from_notification_data(
"worldline",
data,
)
)
received_signature = request.headers.get("X-GCS-Signature")
body = await request.body()
odoo_env[
"shopinvader_provider_worldline.payment_worldline_router.helper"
]._verify_worldline_signature(tx_sudo, received_signature, body)
tx_sudo._handle_notification_data("worldline", data)
except Exception:
_logger.exception("unable to handle worldline notification data", exc_info=True)
return ""


class ShopinvaderApiPaymentProviderWordlineRouterHelper(models.AbstractModel):
_name = "shopinvader_provider_worldline.payment_worldline_router.helper"
_description = "ShopInvader API Payment Provider Worldline Router Helper"

def _verify_worldline_signature(self, tx_sudo, received_signature, data):
"""Verify the Worldline signature."""
try:
WorldlineController._verify_notification_signature(
data, received_signature, tx_sudo
)
except Forbidden as ex:
_logger.exception(ex)
raise ValidationError(_("Unable to verify worldline signature")) from ex