changes
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import main
|
||||
from . import portal
|
||||
|
||||
@@ -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,
|
||||
|
||||
60
fusion_poynt/controllers/portal.py
Normal file
60
fusion_poynt/controllers/portal.py
Normal 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,
|
||||
)
|
||||
Reference in New Issue
Block a user