This commit is contained in:
gsinghpal
2026-04-24 21:04:38 -04:00
parent 0eab4b4efb
commit 41d0908ade
4083 changed files with 1230780 additions and 287 deletions

View File

@@ -208,7 +208,7 @@ class PaymentProvider(models.Model):
json=payload,
params=params,
headers=headers,
timeout=60,
timeout=15,
)
except requests.exceptions.RequestException as e:
_logger.error("Poynt API request failed: %s", e)
@@ -521,6 +521,11 @@ class PaymentProvider(models.Model):
which returns a TransactionReceipt with a ``data`` field containing
the rendered receipt content (HTML or text).
If Poynt has reported a missing receipt template for this business,
we fail fast (no network call) until the admin clears the flag.
This protects Odoo workers from a thundering-herd of 400s when the
merchant's Poynt account is missing its receipt template config.
:param str transaction_id: The Poynt transaction UUID.
:return: The receipt content string, or None on failure.
:rtype: str | None
@@ -528,14 +533,46 @@ class PaymentProvider(models.Model):
self.ensure_one()
if not transaction_id:
return None
ICP = self.env['ir.config_parameter'].sudo()
cooldown_key = f'fusion_poynt.receipt_endpoint_cooldown_until.{self.id}'
try:
cooldown_until = int(ICP.get_param(cooldown_key, '0'))
except (TypeError, ValueError):
cooldown_until = 0
now_ts = int(fields.Datetime.now().timestamp())
if cooldown_until > now_ts:
_logger.debug(
"Skipping Poynt receipt fetch for %s — endpoint in cooldown "
"for %ss (template missing or endpoint unhealthy)",
transaction_id, cooldown_until - now_ts,
)
return None
try:
result = self._poynt_make_request(
'GET', f'transactions/{transaction_id}/receipt',
)
return result.get('data') or None
except (ValidationError, Exception):
except ValidationError as e:
err = str(e).lower()
if 'template' in err or '400' in err:
# Poynt is consistently rejecting the receipt call for this
# business. Put the endpoint in a 1-hour cooldown so subsequent
# webhooks do not hammer Poynt with known-bad requests.
ICP.set_param(cooldown_key, str(now_ts + 3600))
_logger.warning(
"Poynt receipt endpoint returned %s — suspending receipt "
"fetches for provider %s for 1 hour. "
"Admin: check that the Poynt business has a receipt "
"template configured in the Poynt dashboard.",
e, self.id,
)
return None
except Exception:
_logger.debug(
"Could not fetch Poynt receipt for transaction %s", transaction_id,
exc_info=True,
)
return None

View File

@@ -904,11 +904,24 @@ class PaymentTransaction(models.Model):
self.poynt_receipt_data = json.dumps(receipt)
def _poynt_attach_receipt_pdf(self):
"""Render the QWeb receipt report and attach the PDF to the invoice."""
"""Render the QWeb receipt report and attach the PDF to the invoice.
Idempotent: if a PDF for this transaction is already attached,
skip. Poynt fires multiple webhooks per payment; without this
guard the same invoice would get N duplicate receipt PDFs."""
invoice = self.invoice_ids[:1]
if not invoice:
return
filename = f"Payment_Receipt_{self.reference}.pdf"
existing = self.env['ir.attachment'].sudo().search([
('res_model', '=', 'account.move'),
('res_id', '=', invoice.id),
('name', '=', filename),
], limit=1)
if existing:
return
try:
report = self.env.ref('fusion_poynt.action_report_poynt_receipt')
pdf_content, _report_type = report._render_qweb_pdf(report.report_name, [self.id])
@@ -916,7 +929,6 @@ class PaymentTransaction(models.Model):
_logger.debug("Could not render Poynt receipt PDF for %s", self.reference)
return
filename = f"Payment_Receipt_{self.reference}.pdf"
attachment = self.env['ir.attachment'].sudo().create({
'name': filename,
'type': 'binary',
@@ -935,18 +947,33 @@ class PaymentTransaction(models.Model):
)
def _poynt_attach_poynt_receipt(self):
"""Try the Poynt renderReceipt endpoint and attach the result."""
"""Try the Poynt renderReceipt endpoint and attach the result.
Idempotent: if a Poynt HTML receipt for this transaction is already
attached to the invoice, we skip the remote call. Poynt sends
several state-change webhooks per transaction (AUTHORIZED, CAPTURED,
UPDATED...) and each one lands here; without this guard every
webhook would re-fetch the receipt and hammer Poynt.
"""
invoice = self.invoice_ids[:1]
if not invoice:
return
filename = f"Poynt_Receipt_{self.reference}.html"
existing = self.env['ir.attachment'].sudo().search([
('res_model', '=', 'account.move'),
('res_id', '=', invoice.id),
('name', '=', filename),
], limit=1)
if existing:
return
receipt_content = self._get_provider_sudo()._poynt_fetch_receipt(
self.poynt_transaction_id,
)
if not receipt_content:
return
filename = f"Poynt_Receipt_{self.reference}.html"
self.env['ir.attachment'].sudo().create({
'name': filename,
'type': 'binary',
@@ -963,6 +990,11 @@ class PaymentTransaction(models.Model):
1. Sends the invoice via the standard Odoo invoice email template.
2. Sends the Poynt payment receipt email with the PDF attached.
Idempotent: Poynt fires several state-change webhooks per payment
(AUTHORIZED, CAPTURED, UPDATED...). We use `invoice.is_move_sent`
as the "we've already done this" flag so duplicate webhooks don't
re-email the customer.
Best-effort: failures are logged but never block the payment flow.
"""
self.ensure_one()
@@ -976,6 +1008,13 @@ class PaymentTransaction(models.Model):
)
return
if invoice and invoice.is_move_sent:
_logger.debug(
"Skipping auto-send for %s: invoice %s already sent.",
self.reference, invoice.name,
)
return
# 1. Send the invoice PDF
if invoice and invoice.state == 'posted':
try: