This commit is contained in:
gsinghpal
2026-04-29 03:35:33 -04:00
parent 6ac6d24da6
commit a2fe1fcbcc
61 changed files with 4655 additions and 667 deletions

View File

@@ -464,24 +464,26 @@ class CloverPaymentWizard(models.TransientModel):
provider = self._get_provider_sudo()
capture = not provider.capture_manually
minor_amount = clover_utils.format_clover_amount(
self.amount, self.currency_id,
# Tokenize the card client-server FIRST (Clover's tokenization
# endpoint), then charge using only the token. Raw PAN never
# reaches /v1/charges and is never persisted in Odoo.
card_token = provider._clover_tokenize_card(
card_number=self.card_number,
exp_month=int(self.exp_month),
exp_year=int(self.exp_year) if len(self.exp_year) == 4
else 2000 + int(self.exp_year),
cvv=self.cvv,
cardholder_name=self.cardholder_name or '',
)
payload = {
'amount': minor_amount,
'currency': self.currency_id.name.lower(),
'capture': capture,
'ecomind': 'moto',
'description': reference,
'source': self.card_number.replace(' ', ''),
'metadata': {
'odoo_reference': reference,
},
}
result = provider._clover_make_ecom_request(
'POST', 'v1/charges', payload=payload,
result = provider._clover_create_charge(
source_token=card_token,
amount=self.amount,
currency=self.currency_id,
capture=capture,
description=reference,
ecomind='moto',
metadata={'odoo_reference': reference},
)
charge_id = result.get('id', '')
@@ -497,6 +499,11 @@ class CloverPaymentWizard(models.TransientModel):
'clover_charge_id': charge_id,
'clover_status': status,
'source': result.get('source', {}),
# Pass amount + currency back so Odoo 19's amount-tamper
# check (_validate_amount) can verify Clover charged the
# exact amount we asked for.
'amount': result.get('amount'),
'currency': result.get('currency'),
}
if status == 'failed':
@@ -586,6 +593,15 @@ class CloverPaymentWizard(models.TransientModel):
raise UserError(_("Please enter a valid expiry year."))
if not self.cvv or not self.cvv.isdigit():
raise UserError(_("Please enter the CVV."))
# Clover production rejects cards without a 2-part name
# ("Firstname Lastname"). Sandbox is lenient. Validate here so the
# error message is helpful instead of a generic API 400.
if not self.cardholder_name or len(self.cardholder_name.strip().split()) < 2:
raise UserError(_(
"Please enter the cardholder's name as it appears on "
"the card (e.g. \"John Doe\"). Clover requires both "
"first and last name."
))
def _create_payment_transaction(self):
"""Create a payment.transaction linked to the invoice."""

View File

@@ -83,7 +83,8 @@
required="payment_mode == 'card' and state in ('draft', 'error')"
password="True"/>
<field name="cardholder_name"
placeholder="Name on card"/>
placeholder="Firstname Lastname"
required="payment_mode == 'card' and state in ('draft', 'error')"/>
</group>
<group>
<field name="exp_month"