Skip to content

Commit

Permalink
Merge PR #96 into 16.0
Browse files Browse the repository at this point in the history
Signed-off-by lmignon
  • Loading branch information
shopinvader-git-bot committed Jun 3, 2024
2 parents 6e4a633 + d5e7af4 commit 45e52bb
Show file tree
Hide file tree
Showing 82 changed files with 2,760 additions and 0 deletions.
5 changes: 5 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# generated from manifests external_dependencies
extendable-pydantic>=1.2.0
fastapi
pydantic>=2.0.0
pyjwt
2 changes: 2 additions & 0 deletions setup/.setuptools-odoo-make-default-ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# addons listed in this file are ignored by
# setuptools-odoo-make-default (one addon per line)
2 changes: 2 additions & 0 deletions setup/README
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
To learn more about this directory, please visit
https://pypi.python.org/pypi/setuptools-odoo
6 changes: 6 additions & 0 deletions setup/shopinvader_api_payment/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,
)
6 changes: 6 additions & 0 deletions setup/shopinvader_api_payment_cart/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,
)
6 changes: 6 additions & 0 deletions setup/shopinvader_api_payment_provider_custom/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,
)
6 changes: 6 additions & 0 deletions setup/shopinvader_api_payment_provider_sips/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,
)
6 changes: 6 additions & 0 deletions setup/shopinvader_api_payment_provider_stripe/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/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import models
from . import routers
28 changes: 28 additions & 0 deletions shopinvader_api_payment/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Copyright 2024 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

{
"name": "Shopinvader Api Payment",
"summary": """
Shopinvader services to be able to pay (invoices, carts,...)""",
"version": "16.0.1.0.0",
"license": "AGPL-3",
"author": "ACSONE SA/NV,Shopinvader",
"website": "https://github.com/shopinvader/odoo-shopinvader-payment",
"depends": [
"fastapi",
"payment",
"payment_sips",
"pydantic",
"extendable",
"extendable_fastapi",
],
"external_dependencies": {
"python": [
"fastapi",
"pydantic>=2.0.0",
"extendable-pydantic>=1.2.0",
"pyjwt",
]
},
}
1 change: 1 addition & 0 deletions shopinvader_api_payment/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import payment_transaction
13 changes: 13 additions & 0 deletions shopinvader_api_payment/models/payment_transaction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright 2024 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import fields, models


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

shopinvader_frontend_redirect_url = fields.Char(
string="Shopinvader Frontend Redirect URL",
help="URL where the frontend should be redirected after the payment processing",
)
2 changes: 2 additions & 0 deletions shopinvader_api_payment/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
* Marie Lejeune <[email protected]>
* Stéphane Bidoul <[email protected]>
28 changes: 28 additions & 0 deletions shopinvader_api_payment/readme/USAGE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
This addon is the core of the new Shopinvader API Payment addons suite.
It defines basic services, which will be extended in two axes.

* The first axe concerns the payable object. Here the methods should work with any abstract payable object (sale order, account invoice, ...) but specific logic must be implemented in related addons (see `shopinvader_api_payment_cart` to pay sale orders for eg.)
* The second axe concerns the payment provider. The idea is to develop one addon for each payment provider. Some of them are already available, see `shopinvader_api_payment_sips`, `shopinvader_api_payment_stripe`, `shopinvader_api_payment_custom`. In these addons we add the necessary logic to redirect to the payment provider payment website, the return url ...

All payment routes are public. We must thus encode all sensitive info.
The `Payable` object achieves this. In each service we ensure that the payable
wasn't tampered.

**Concrete Usage**

The idea to use this suite of addons is the following. Assume you have a valid
payable (see addons of the first axe on how to get them, for eg. `shopinvader_api_payment_cart`
on how to get the payable of the current cart).

1. Get all providers that are allowed to pay your payable object.
You just need to call the GET route `/payment/methods` with your payable for this.

2. Once you chose the payment method you want to use, create the payment transaction
calling the POST route `/payment/transactions` with your payable + some
additional input info (the chosen provider, the frontend redirect url...).
See the associated `TransactionCreate` Pydantic schema.

3. The following (and last) step depends on the chosen provider. See more info
into the dedicated Shopinvader API payment addon.
However, the idea is often the same: a `redirect_form_html` is returned and
you should submit this HTML form to call the provider services.
1 change: 1 addition & 0 deletions shopinvader_api_payment/routers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .payment import payment_router
223 changes: 223 additions & 0 deletions shopinvader_api_payment/routers/payment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
# Copyright Odoo SA (https://odoo.com)
# Copyright 2024 ACSONE SA (https://acsone.eu).
# @author Stéphane Bidoul <[email protected]>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

import logging
from typing import Annotated, Any
from urllib.parse import urljoin

from fastapi import APIRouter, Depends, HTTPException, Request

from odoo import api, models

from odoo.addons.fastapi.dependencies import odoo_env
from odoo.addons.payment.models.payment_provider import PaymentProvider
from odoo.addons.payment.models.payment_transaction import PaymentTransaction

from ..schemas import (
PaymentDataWithMethods,
TransactionCreate,
TransactionProcessingValues,
)
from ..schemas import (
PaymentProvider as PaymentProviderSchema,
)
from .utils import Payable

_logger = logging.getLogger(__name__)

payment_router = APIRouter(tags=["payment"])


@payment_router.get("/payment/methods")
def pay(
payable: str,
odoo_env: Annotated[api.Environment, Depends(odoo_env)],
) -> PaymentDataWithMethods:
"""Available payment providers for the given encoded payment data.
This route is public, so it is possible to pay anonymously provided that the
parameters are obtained securely by another mean. An authenticated user can
obtain the parameters with corresponding routes on the related payable
objects (/cart/current/payable for e.g.).
"""
try:
payable_obj = Payable.decode(odoo_env, payable)
except Exception as e:
_logger.info("Could not decode payable")
raise HTTPException(403) from e
# This method is similar to Odoo's PaymentPortal.payment_pay
providers_sudo = (
odoo_env["payment.provider"]
.sudo()
._get_compatible_providers(
payable_obj.company_id,
payable_obj.partner_id,
payable_obj.amount,
currency_id=payable_obj.currency_id,
)
)
return PaymentDataWithMethods(
payable=payable,
payable_reference=payable_obj.payable_reference,
amount=payable_obj.amount,
currency_code=odoo_env["res.currency"].browse(payable_obj.currency_id).name,
# We assume that the payable model has a currency field.
# This shouldn't be a big assumption
amount_formatted=odoo_env[payable_obj.payable_model]
.sudo()
.browse(payable_obj.payable_id)
.currency_id.format(payable_obj.amount),
providers=[
PaymentProviderSchema.from_payment_provider(provider)
for provider in providers_sudo
],
)


@payment_router.post("/payment/transactions")
def transaction(
data: TransactionCreate,
request: Request,
odoo_env: Annotated[api.Environment, Depends(odoo_env)],
) -> TransactionProcessingValues:
"""Create a payment transaction.
Input is data obtained from /payment/providers, with the provider selected by the
user. This route is public, so it is possible to pay anonymously.
This route will automatically redirect to the return route linked to
the specified provider. The user will finally land on data.frontend_redirect_url
"""
try:
payable_obj = Payable.decode(odoo_env, data.payable)
except Exception as e:
_logger.info("Could not decode payable")
raise HTTPException(403) from e
# similar to Odoo's /payment/transaction route
if data.flow == "redirect":
providers_sudo = (
odoo_env["payment.provider"]
.sudo()
._get_compatible_providers(
payable_obj.company_id,
payable_obj.partner_id,
payable_obj.amount,
currency_id=payable_obj.currency_id,
)
)
if not data.provider_id or data.provider_id not in providers_sudo.ids:
_logger.info(
"Invalid provider %s for partner %s",
data.provider_id,
payable_obj.partner_id,
)
raise HTTPException(403)
provider_sudo = odoo_env["payment.provider"].sudo().browse(data.provider_id)

# Create the transaction
tx_sudo = odoo_env[
"shopinvader_api_payment.payment_router.helper"
]._create_transaction(data, provider_sudo, request, odoo_env)
tx_sudo._log_sent_message()

transaction_processing_values = odoo_env[
"shopinvader_api_payment.payment_router.helper"
]._get_tx_processing_values(
tx_sudo,
payable=data.payable,
frontend_redirect_url=data.frontend_redirect_url,
)
return transaction_processing_values
else:
raise NotImplementedError("Only redirect flow is supported")


class ShopinvaderApiPaymentRouterHelper(models.AbstractModel):
_name = "shopinvader_api_payment.payment_router.helper"
_description = "ShopInvader API Payment Router Helper"

def _get_additional_transaction_create_values(
self,
data: TransactionCreate,
odoo_env: Annotated[api.Environment, Depends(odoo_env)],
) -> dict:
# Intended to be extended for invoices, carts...
additional_transaction_create_values = {}
return additional_transaction_create_values

def _get_tx_create_values(
self,
data: TransactionCreate,
provider_sudo: PaymentProvider,
odoo_env: Annotated[api.Environment, Depends(odoo_env)],
) -> dict:
try:
payable_obj = Payable.decode(odoo_env, data.payable)
except Exception as e:
_logger.info("Could not decode payable")
raise HTTPException(403) from e
additional_transaction_create_values = (
self._get_additional_transaction_create_values(data, odoo_env)
)

is_validation = False # future
# compute transaction reference from payable reference
tx_reference = (
odoo_env["payment.transaction"]
.sudo()
._compute_reference(
provider_code=provider_sudo.code,
prefix=payable_obj.payable_reference,
# TODO are custom_create_values and kwargs really needed
# **(custom_create_values or {}),
# **kwargs
)
)

return {
"provider_id": data.provider_id,
"reference": tx_reference,
"amount": payable_obj.amount,
"currency_id": payable_obj.currency_id,
"partner_id": payable_obj.partner_id,
"shopinvader_frontend_redirect_url": data.frontend_redirect_url,
# 'token_id': token_id,
"operation": f"online_{data.flow}" if not is_validation else "validation",
"tokenize": False,
**additional_transaction_create_values,
}

def _create_transaction(
self,
data: TransactionCreate,
provider_sudo: PaymentProvider,
request: Request,
odoo_env: Annotated[api.Environment, Depends(odoo_env)],
) -> dict:
transaction_values = odoo_env[
"shopinvader_api_payment.payment_router.helper"
]._get_tx_create_values(data, provider_sudo, odoo_env)
tx_sudo = (
odoo_env["payment.transaction"]
.sudo()
.with_context(
shopinvader_api_payment=True,
shopinvader_api_payment_base_url=urljoin(
str(request.url), "providers/"
),
)
.create(transaction_values)
)
return tx_sudo

def _get_tx_processing_values(
self, tx_sudo: PaymentTransaction, **kwargs: Any
) -> TransactionProcessingValues:
"""
Extract the creation of the response to allow to extend it.
"""
return TransactionProcessingValues(
flow="redirect", **tx_sudo._get_processing_values()
)
Loading

0 comments on commit 45e52bb

Please sign in to comment.