TNCSC_Odoo/addons/community_interac/models/payment_transaction.py
metatroncubeswdev 7e860b65c5 feat(community_interac): Interac e-Transfer payment provider (Phase 5)
The plan flags this as the highest-risk module since the payment provider
API is strict and version-sensitive. Before writing any code, read Odoo
19's own payment_custom module (its wire-transfer provider) end to end as
a reference, since it's the closest first-party analog to a manual/
offline payment flow - this avoided the trial-and-error that hit the
other modules and got the core logic right on the first install attempt.

payment.provider gains code='interac' (via selection_add, same pattern
payment_custom uses for 'custom') plus interac_recipient_email
(required_if_provider='interac' - Odoo only enforces this when the
provider's state is enabled/test, so the module ships a disabled,
unconfigured provider record and the deployment layer configures + enables
it, keeping client specifics out of product code) and a configurable
interac_deadline_hours.

Flow: selecting Interac at checkout calls _apply_updates, which sets the
transaction 'pending' and emails instructions (recipient address, amount,
reference, deadline) via a mail.template - no dynamic per-transaction data
needs to live in the static provider-level pending_msg field, since the
reference/amount are already shown on Odoo's generic payment status page.
A "Pending Interac Payments" admin list (Interac Payment Verifier group)
has a one-click "Payment Received" button calling action_confirm_interac_
payment (-> _set_done, which triggers Odoo's normal order/invoice
reconciliation - no need to reimplement that). An hourly cron cancels
unconfirmed pending transactions past the deadline and emails a
cancellation notice.

Verified against a live Odoo 19 + Postgres 16 container: 4/4 automated
tests pass, plus a full manual live run of both cycles the plan's gate
asks for - drove a transaction through the actual /payment/interac/process
controller to pending (confirmed the instructions email), used the
treasurer action to confirm it to 'done', and separately backdated a
second transaction's last_state_change and triggered the auto-cancel cron
via ir.cron's method_direct_trigger, confirming both the state change to
'cancel' and the cancellation email.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 21:59:15 -04:00

62 lines
2.5 KiB
Python

from odoo import fields, models
from odoo.addons.payment.logging import get_payment_logger
from odoo.addons.community_interac.controllers.main import InteracController
_logger = get_payment_logger(__name__)
class PaymentTransaction(models.Model):
_inherit = 'payment.transaction'
def _get_specific_rendering_values(self, processing_values):
if self.provider_code != 'interac':
return super()._get_specific_rendering_values(processing_values)
return {
'api_url': InteracController._process_url,
'reference': self.reference,
}
def _extract_amount_data(self, payment_data):
"""Override of `payment` to skip amount validation - there is no external gateway response."""
if self.provider_code != 'interac':
return super()._extract_amount_data(payment_data)
return None
def _apply_updates(self, payment_data):
if self.provider_code != 'interac':
return super()._apply_updates(payment_data)
_logger.info("Interac transaction %s selected by customer: set as pending.", self.reference)
self._set_pending()
self._send_interac_instructions_email()
def _send_interac_instructions_email(self):
self.ensure_one()
template = self.env.ref('community_interac.mail_template_interac_instructions', raise_if_not_found=False)
if template:
template.send_mail(self.id, force_send=False)
def _send_interac_cancel_email(self):
self.ensure_one()
template = self.env.ref('community_interac.mail_template_interac_cancelled', raise_if_not_found=False)
if template:
template.send_mail(self.id, force_send=False)
def action_confirm_interac_payment(self):
"""One-click admin/treasurer confirmation that the e-transfer was received."""
for tx in self:
if tx.provider_code == 'interac' and tx.state == 'pending':
tx._set_done()
return True
def _cron_auto_cancel_interac(self):
pending_interac = self.search([('provider_code', '=', 'interac'), ('state', '=', 'pending')])
for tx in pending_interac:
deadline_hours = tx.provider_id.interac_deadline_hours or 48
elapsed_hours = (fields.Datetime.now() - tx.last_state_change).total_seconds() / 3600.0
if elapsed_hours >= deadline_hours:
tx._set_canceled(state_message="Auto-cancelled: Interac payment not confirmed within the deadline.")
tx._send_interac_cancel_email()
return True