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

@@ -5,4 +5,5 @@ from . import payment_provider
from . import payment_token
from . import payment_transaction
from . import poynt_terminal
from . import res_config_settings
from . import sale_order

View File

@@ -347,6 +347,21 @@ class PaymentProvider(models.Model):
and payment_method_sudo.support_tokenization
),
}
ICP = self.env['ir.config_parameter'].sudo()
surcharge_enabled = ICP.get_param(
'fusion_poynt.surcharge_enabled', 'False',
) == 'True'
if surcharge_enabled:
inline_form_values['surcharge'] = {
'enabled': True,
'visa': float(ICP.get_param('fusion_poynt.surcharge_visa_rate', '0') or 0),
'mastercard': float(ICP.get_param('fusion_poynt.surcharge_mastercard_rate', '0') or 0),
'amex': float(ICP.get_param('fusion_poynt.surcharge_amex_rate', '0') or 0),
'debit': float(ICP.get_param('fusion_poynt.surcharge_debit_rate', '0') or 0),
'other': float(ICP.get_param('fusion_poynt.surcharge_other_rate', '0') or 0),
}
return json.dumps(inline_form_values)
# === ACTION METHODS === #

View File

@@ -92,13 +92,20 @@ class PaymentTransaction(models.Model):
poynt_data = self._poynt_create_order_and_authorize()
if poynt_data:
status = poynt_data.get('status', 'AUTHORIZED')
payment_data = {
'reference': self.reference,
'poynt_order_id': poynt_data.get('order_id'),
'poynt_transaction_id': poynt_data.get('transaction_id'),
'poynt_status': poynt_data.get('status', 'AUTHORIZED'),
'poynt_status': status,
'funding_source': poynt_data.get('funding_source', {}),
}
if status in ('DECLINED', 'FAILED', 'REFUND_FAILED'):
self._set_error(
_("Payment was %(status)s by the processor.",
status=status.lower())
)
return
self._process('poynt', payment_data)
def _poynt_create_order_and_authorize(self):
@@ -148,6 +155,103 @@ class PaymentTransaction(models.Model):
self._set_error(str(e))
return {}
@staticmethod
def _detect_card_brand_from_details(payment_details):
"""Detect card brand from the payment_details string on a token.
Tokens store details like "VISA ending in 1234" or
"AMERICAN_EXPRESS ending in 5678".
"""
details = (payment_details or '').upper()
if 'AMEX' in details or 'AMERICAN_EXPRESS' in details:
return 'amex'
if 'VISA' in details:
return 'visa'
if 'MASTER' in details:
return 'mastercard'
return 'other'
def _apply_token_surcharge(self):
"""Apply surcharge to the linked invoice for token-based payments.
Checks if surcharge is enabled, detects card brand from the token,
adds a surcharge line to the invoice, and updates the transaction
amount. Skips rental orders (recurring charges should not get
surcharge), invoices with no linked records, or invoices where
surcharge is already applied.
"""
ICP = self.env['ir.config_parameter'].sudo()
if ICP.get_param('fusion_poynt.surcharge_enabled', 'False') != 'True':
return
if not self.token_id or not self.invoice_ids:
return
for inv in self.invoice_ids:
sale_orders = inv.mapped('line_ids.sale_line_ids.order_id')
for so in sale_orders:
if getattr(so, 'is_rental_order', False):
if not getattr(so, 'rental_apply_cc_fee', True):
return
card_type = self._detect_card_brand_from_details(
self.token_id.payment_details,
)
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
product_id = int(ICP.get_param('fusion_poynt.surcharge_product_id', '0') or 0)
product = self.env['product.product'].sudo().browse(product_id).exists()
if not product:
product = self.env.ref(
'fusion_poynt.product_cc_processing_fee', raise_if_not_found=False,
)
if not product:
_logger.warning("Surcharge product not configured; skipping token surcharge")
return
total_fee = 0.0
for invoice in self.invoice_ids.sudo():
already_has = invoice.invoice_line_ids.filtered(
lambda l: l.product_id.id == product.id
)
if already_has:
continue
fee_amount = round(invoice.amount_residual * rate / 100.0, 2)
if fee_amount <= 0:
continue
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()
total_fee += fee_amount
if total_fee > 0:
self.amount += total_fee
def _poynt_process_token_payment(self):
"""Process a payment using a stored token (card on file).
@@ -156,6 +260,8 @@ class PaymentTransaction(models.Model):
were created before the JWT migration.
"""
try:
self._apply_token_surcharge()
provider = self._get_provider_sudo()
action = 'AUTHORIZE' if provider.capture_manually else 'SALE'
payment_jwt = self.token_id.poynt_payment_token
@@ -213,13 +319,28 @@ class PaymentTransaction(models.Model):
if order_id:
self.poynt_order_id = order_id
status = txn_result.get('status', '')
payment_data = {
'reference': self.reference,
'poynt_order_id': order_id,
'poynt_transaction_id': transaction_id,
'poynt_status': txn_result.get('status', ''),
'poynt_status': status,
'funding_source': txn_result.get('fundingSource', {}),
}
if status in ('DECLINED', 'FAILED', 'REFUND_FAILED'):
processor = txn_result.get('processorResponse', {})
decline_msg = (
processor.get('statusMessage')
or processor.get('message')
or status.lower()
)
self._set_error(
_("Payment %(status)s: %(reason)s",
status=status.lower(), reason=decline_msg)
)
return
self._process('poynt', payment_data)
except ValidationError as e:
self._set_error(str(e))

View File

@@ -0,0 +1,91 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
poynt_surcharge_enabled = fields.Boolean(
string="Enable Credit Card Surcharge",
config_parameter='fusion_poynt.surcharge_enabled',
)
poynt_surcharge_visa_rate = fields.Float(
string="Visa Rate (%)",
config_parameter='fusion_poynt.surcharge_visa_rate',
default=2.5,
)
poynt_surcharge_mastercard_rate = fields.Float(
string="Mastercard Rate (%)",
config_parameter='fusion_poynt.surcharge_mastercard_rate',
default=2.5,
)
poynt_surcharge_amex_rate = fields.Float(
string="Amex Rate (%)",
config_parameter='fusion_poynt.surcharge_amex_rate',
default=3.5,
)
poynt_surcharge_debit_rate = fields.Float(
string="Debit Rate (%)",
config_parameter='fusion_poynt.surcharge_debit_rate',
default=0.0,
)
poynt_surcharge_other_rate = fields.Float(
string="Other Cards Rate (%)",
config_parameter='fusion_poynt.surcharge_other_rate',
default=2.5,
)
poynt_surcharge_product_id = fields.Many2one(
'product.product',
string="Surcharge Product",
config_parameter='fusion_poynt.surcharge_product_id',
help="The service product used for the credit card processing fee line.",
)
@api.model
def get_values(self):
res = super().get_values()
ICP = self.env['ir.config_parameter'].sudo()
product_id = int(ICP.get_param('fusion_poynt.surcharge_product_id', '0') or 0)
if product_id and self.env['product.product'].sudo().browse(product_id).exists():
res['poynt_surcharge_product_id'] = product_id
else:
default = self.env.ref('fusion_poynt.product_cc_processing_fee', raise_if_not_found=False)
res['poynt_surcharge_product_id'] = default.id if default else False
return res
def set_values(self):
super().set_values()
ICP = self.env['ir.config_parameter'].sudo()
ICP.set_param(
'fusion_poynt.surcharge_product_id',
str(self.poynt_surcharge_product_id.id) if self.poynt_surcharge_product_id else '0',
)
def action_open_poynt_provider(self):
provider = self.env['payment.provider'].sudo().search(
[('code', '=', 'poynt')], limit=1,
)
if provider:
return {
'type': 'ir.actions.act_window',
'res_model': 'payment.provider',
'res_id': provider.id,
'view_mode': 'form',
'target': 'current',
}
return {
'type': 'ir.actions.act_window',
'res_model': 'payment.provider',
'view_mode': 'list,form',
'target': 'current',
'domain': [('code', '=', 'poynt')],
}
def action_open_poynt_terminals(self):
return {
'type': 'ir.actions.act_window',
'res_model': 'poynt.terminal',
'view_mode': 'list,form',
'target': 'current',
}