This commit is contained in:
gsinghpal
2026-03-20 11:46:41 -04:00
parent 595dccc17d
commit 92369be6e0
71 changed files with 6588 additions and 8 deletions

View File

@@ -1,3 +1,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main
from . import portal

View File

@@ -19,6 +19,25 @@ from odoo.addons.fusion_poynt import utils as poynt_utils
_logger = logging.getLogger(__name__)
def _detect_card_brand(card_number):
"""Detect the card brand from the card number using BIN prefixes."""
num = (card_number or '').replace(' ', '')
if len(num) < 2:
return 'other'
if num[:2] in ('34', '37'):
return 'amex'
if num[0] == '4':
return 'visa'
prefix2 = int(num[:2])
if 51 <= prefix2 <= 55:
return 'mastercard'
if len(num) >= 4:
prefix4 = int(num[:4])
if 2221 <= prefix4 <= 2720:
return 'mastercard'
return 'other'
class PoyntController(http.Controller):
_return_url = '/payment/poynt/return'
_webhook_url = '/payment/poynt/webhook'
@@ -344,6 +363,81 @@ class PoyntController(http.Controller):
return request.redirect('/odoo/settings')
# === SURCHARGE HELPER === #
def _apply_portal_surcharge(self, tx_sudo, card_type):
"""Apply credit card surcharge to the linked invoice if enabled.
Detects the card brand from the number if card_type is not provided,
adds a surcharge line to the invoice, and updates the transaction
amount to include the fee.
:param tx_sudo: The sudo payment.transaction record.
:param str card_type: The card brand (visa, mastercard, amex, other).
:return: The surcharge fee amount, or 0 if not applied.
:rtype: float
"""
ICP = request.env['ir.config_parameter'].sudo()
if ICP.get_param('fusion_poynt.surcharge_enabled', 'False') != 'True':
return 0.0
if not card_type:
card_type = 'other'
rate_key = {
'visa': 'fusion_poynt.surcharge_visa_rate',
'mastercard': 'fusion_poynt.surcharge_mastercard_rate',
'amex': 'fusion_poynt.surcharge_amex_rate',
'debit': 'fusion_poynt.surcharge_debit_rate',
}.get(card_type, 'fusion_poynt.surcharge_other_rate')
rate = float(ICP.get_param(rate_key, '0') or 0)
if rate <= 0:
return 0.0
invoices = tx_sudo.invoice_ids
if not invoices:
base_amount = tx_sudo.amount
else:
base_amount = sum(invoices.mapped('amount_residual'))
fee_amount = round(base_amount * rate / 100.0, 2)
if fee_amount <= 0:
return 0.0
product_id = int(ICP.get_param('fusion_poynt.surcharge_product_id', '0') or 0)
product = request.env['product.product'].sudo().browse(product_id).exists()
if not product:
product = request.env.ref(
'fusion_poynt.product_cc_processing_fee', raise_if_not_found=False,
)
if not product:
_logger.warning("Surcharge product not configured; skipping surcharge")
return 0.0
for invoice in invoices.sudo():
was_posted = invoice.state == 'posted'
if was_posted:
invoice.button_draft()
description = "Credit Card Processing Fee (%.2f%% surcharge)" % rate
invoice.write({
'invoice_line_ids': [(0, 0, {
'product_id': product.id,
'name': description,
'quantity': 1,
'price_unit': fee_amount,
'tax_ids': [(5, 0, 0)],
})],
})
if was_posted:
invoice.action_post()
new_amount = tx_sudo.amount + fee_amount
tx_sudo.write({'amount': new_amount})
return fee_amount
# === JSON-RPC ROUTES (called from frontend JS) === #
@http.route('/payment/poynt/terminals', type='jsonrpc', auth='public')
@@ -372,7 +466,8 @@ class PoyntController(http.Controller):
@http.route('/payment/poynt/process_card', type='jsonrpc', auth='public')
def poynt_process_card(self, reference=None, poynt_order_id=None,
card_number=None, exp_month=None, exp_year=None,
cvv=None, cardholder_name=None, **kwargs):
cvv=None, cardholder_name=None, card_type=None,
**kwargs):
"""Process a card payment through Poynt Cloud API.
The frontend sends card details which are passed to Poynt for
@@ -393,6 +488,11 @@ class PoyntController(http.Controller):
return {'error': 'Transaction not found.'}
try:
if not card_type and card_number:
card_type = _detect_card_brand(card_number)
surcharge_fee = self._apply_portal_surcharge(tx_sudo, card_type)
funding_source = {
'type': 'CREDIT_DEBIT',
'card': {
@@ -469,7 +569,7 @@ class PoyntController(http.Controller):
@http.route('/payment/poynt/send_to_terminal', type='jsonrpc', auth='public')
def poynt_send_to_terminal(self, reference=None, terminal_id=None,
poynt_order_id=None, **kwargs):
poynt_order_id=None, card_type=None, **kwargs):
"""Send a payment request to a Poynt terminal device.
:return: Dict with success status or error message.
@@ -491,6 +591,8 @@ class PoyntController(http.Controller):
return {'error': 'Terminal not found.'}
try:
surcharge_fee = self._apply_portal_surcharge(tx_sudo, card_type or 'other')
result = terminal.action_send_payment_to_terminal(
amount=tx_sudo.amount,
currency=tx_sudo.currency_id,

View File

@@ -0,0 +1,60 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import http
from odoo.http import request
from odoo.addons.sale.controllers.portal import CustomerPortal
class PoyntCustomerPortal(CustomerPortal):
@http.route()
def portal_order_page(
self,
order_id,
report_type=None,
access_token=None,
message=False,
download=False,
payment_amount=None,
amount_selection=None,
**kw
):
"""Auto-inject payment_amount for confirmed orders with outstanding balance.
For confirmed sale orders (state == 'sale') that haven't been fully
paid, this automatically sets payment_amount to the remaining balance
so that the standard portal "Pay Now" button appears without requiring
a separate payment link URL.
Rental orders are excluded -- their payment flow is managed by
fusion_rental.
"""
if payment_amount is None:
try:
order_sudo = self._document_check_access(
'sale.order', order_id, access_token=access_token,
)
except Exception:
order_sudo = None
if order_sudo:
is_rental = getattr(order_sudo, 'is_rental_order', False)
if (
order_sudo.state == 'sale'
and not is_rental
and order_sudo.amount_total > 0
and order_sudo.amount_paid < order_sudo.amount_total
):
payment_amount = order_sudo.amount_total - order_sudo.amount_paid
return super().portal_order_page(
order_id,
report_type=report_type,
access_token=access_token,
message=message,
download=download,
payment_amount=payment_amount,
amount_selection=amount_selection,
**kw,
)