import logging from odoo import _, fields, http from odoo.http import request from odoo.exceptions import UserError _logger = logging.getLogger(__name__) class FusionRentalController(http.Controller): # ================================================================= # Cancellation (from renewal reminder email) # ================================================================= @http.route( '/rental/cancel/', type='http', auth='public', website=True, methods=['GET', 'POST'], ) def rental_cancel(self, token, **kwargs): """Handle rental cancellation requests from email links.""" cancel_request = request.env['rental.cancellation.request'].sudo().search([ ('token', '=', token), ('state', '=', 'new'), ], limit=1) if not cancel_request: return request.render( 'fusion_rental.cancellation_invalid_page', {'error': _("This cancellation link is invalid or has already been used.")}, ) order = cancel_request.order_id today = fields.Date.today() if order.rental_next_renewal_date and order.rental_next_renewal_date <= today: return request.render( 'fusion_rental.cancellation_invalid_page', {'error': _( "Cancellation is not allowed on or after the renewal date. " "Please contact our office for assistance." )}, ) if request.httprequest.method == 'POST': reason = kwargs.get('reason', '') cancel_request.write({'reason': reason}) cancel_request.action_confirm() return request.render( 'fusion_rental.cancellation_success_page', { 'order': order, 'partner': cancel_request.partner_id, }, ) return request.render( 'fusion_rental.cancellation_form_page', { 'order': order, 'partner': cancel_request.partner_id, 'token': token, }, ) # ================================================================= # Portal: Confirm Quotation → Redirect to Rental Agreement # ================================================================= @http.route( '/rental/confirm-and-sign/', type='http', auth='public', website=True, methods=['GET', 'POST'], ) def rental_confirm_and_sign(self, order_id, access_token=None, **kwargs): """Confirm a rental quotation from the portal and redirect to the rental agreement signing page. This replaces Odoo's standard 'Sign & Pay' flow for rental orders. Uses the sale order's access_token for authentication so customers don't need a portal account. """ order = request.env['sale.order'].sudo().browse(order_id) if ( not order.exists() or not order.is_rental_order or not access_token or order.access_token != access_token ): return request.redirect('/my/orders') if order.state in ('draft', 'sent'): order.action_confirm() if not order.rental_agreement_token: import uuid order.rental_agreement_token = uuid.uuid4().hex return request.redirect( f'/rental/agreement/{order.id}/{order.rental_agreement_token}' ) # ================================================================= # Rental Agreement Signing # ================================================================= @http.route( '/rental/agreement//', type='http', auth='public', website=True, methods=['GET'], ) def rental_agreement_page(self, order_id, token, **kwargs): """Render the rental agreement signing page.""" order = request.env['sale.order'].sudo().browse(order_id) if ( not order.exists() or order.rental_agreement_token != token or order.rental_agreement_signed ): return request.render( 'fusion_rental.cancellation_invalid_page', {'error': _("This agreement link is invalid or has already been signed.")}, ) google_api_key = request.env['ir.config_parameter'].sudo().get_param( 'fusion_claims.google_maps_api_key', '' ) if not google_api_key: google_api_key = request.env['ir.config_parameter'].sudo().get_param( 'fusion_rental.google_maps_api_key', '' ) poynt_business_id = '' poynt_application_id = '' provider = request.env['payment.provider'].sudo().search([ ('code', '=', 'poynt'), ('state', '!=', 'disabled'), ], limit=1) if provider: poynt_business_id = provider.poynt_business_id or '' raw_app_id = provider.poynt_application_id or '' from odoo.addons.fusion_poynt.utils import clean_application_id poynt_application_id = clean_application_id(raw_app_id) or raw_app_id return request.render( 'fusion_rental.agreement_signing_page', { 'order': order, 'partner': order.partner_id, 'token': token, 'pdf_preview_url': f'/rental/agreement/{order_id}/{token}/pdf', 'google_api_key': google_api_key, 'poynt_business_id': poynt_business_id, 'poynt_application_id': poynt_application_id, }, ) @http.route( '/rental/agreement///pdf', type='http', auth='public', methods=['GET'], ) def rental_agreement_pdf(self, order_id, token, **kwargs): """Serve the rental agreement PDF for preview (token-protected).""" order = request.env['sale.order'].sudo().browse(order_id) if not order.exists() or order.rental_agreement_token != token: return request.not_found() report_name = 'fusion_rental.report_rental_agreement' report = request.env['ir.actions.report'].sudo()._get_report_from_name(report_name) if not report: report = request.env['ir.actions.report'].sudo()._get_report_from_name( 'fusion_claims.report_rental_agreement' ) report_name = 'fusion_claims.report_rental_agreement' if not report: return request.not_found() pdf_content, _report_type = report.sudo()._render_qweb_pdf( report_name, [order.id] ) return request.make_response( pdf_content, headers=[ ('Content-Type', 'application/pdf'), ('Content-Disposition', f'inline; filename="{order.name} - {order.partner_id.name or ""}' f' - Rental Agreement' f'{" - Signed" if order.rental_agreement_signed else ""}.pdf"'), ], ) @http.route( '/rental/agreement///sign', type='json', auth='public', methods=['POST'], ) def rental_agreement_sign(self, order_id, token, **kwargs): """Process the agreement signing: save signature and tokenize card via nonce.""" order = request.env['sale.order'].sudo().browse(order_id) if ( not order.exists() or order.rental_agreement_token != token or order.rental_agreement_signed ): return {'success': False, 'error': 'Invalid or expired agreement link.'} signer_name = kwargs.get('signer_name', '').strip() signature_data = kwargs.get('signature_data', '') nonce = kwargs.get('nonce', '').strip() billing_address = kwargs.get('billing_address', '').strip() billing_city = kwargs.get('billing_city', '').strip() billing_state = kwargs.get('billing_state', '').strip() billing_postal_code = kwargs.get('billing_postal_code', '').strip() if not signer_name: return {'success': False, 'error': 'Full name is required.'} if not signature_data: return {'success': False, 'error': 'Signature is required.'} if not nonce: return {'success': False, 'error': 'Card authorization is required. Please fill in your card details.'} if not billing_postal_code: return {'success': False, 'error': 'Billing postal/zip code is required.'} sig_binary = signature_data if ',' in sig_binary: sig_binary = sig_binary.split(',')[1] full_billing = billing_address if billing_city: full_billing += f", {billing_city}" if billing_state: full_billing += f", {billing_state}" try: payment_token = self._tokenize_nonce_via_poynt( order, nonce, billing_postal_code, ) except (UserError, Exception) as e: _logger.error("Nonce tokenization failed for %s: %s", order.name, e) return {'success': False, 'error': str(e)} order.write({ 'rental_agreement_signed': True, 'rental_agreement_signature': sig_binary, 'rental_agreement_signer_name': signer_name, 'rental_agreement_signed_date': fields.Datetime.now(), 'rental_payment_token_id': payment_token.id if payment_token else False, 'rental_billing_address': full_billing, 'rental_billing_postal_code': billing_postal_code, }) order.message_post( body=_("Rental agreement signed by %s.", signer_name), ) try: order._generate_and_attach_signed_agreement() except Exception as e: _logger.error( "Signed agreement PDF generation failed for %s: %s", order.name, e, ) try: order._process_post_signing_payments() except Exception as e: _logger.error( "Post-signing payment processing failed for %s: %s", order.name, e, ) return { 'success': True, 'message': 'Agreement signed successfully. Thank you!', } @http.route( '/rental/agreement///decline', type='json', auth='public', methods=['POST'], ) def rental_agreement_decline(self, order_id, token, **kwargs): """Cancel/decline the rental order from the agreement page.""" order = request.env['sale.order'].sudo().browse(order_id) if ( not order.exists() or order.rental_agreement_token != token or order.rental_agreement_signed ): return {'success': False, 'error': 'Invalid or expired agreement link.'} try: order.action_cancel() order.message_post( body=_("Rental order declined by the customer from the agreement page."), ) except Exception as e: _logger.error("Agreement decline failed for %s: %s", order.name, e) return {'success': False, 'error': 'Unable to cancel the order. Please contact our office.'} return { 'success': True, 'message': 'Your rental order has been cancelled. No charges have been applied.', } @http.route( '/rental/agreement///thank-you', type='http', auth='public', website=True, methods=['GET'], ) def rental_agreement_thank_you(self, order_id, token, **kwargs): """Render the thank-you page after successful agreement signing.""" order = request.env['sale.order'].sudo().browse(order_id) if ( not order.exists() or order.rental_agreement_token != token ): return request.render( 'fusion_rental.cancellation_invalid_page', {'error': _("This link is invalid.")}, ) company = order.company_id or request.env.company return request.render( 'fusion_rental.agreement_thank_you_page', { 'order': order, 'partner': order.partner_id, 'company_phone': company.phone or '', 'company_email': company.email or '', }, ) def _tokenize_nonce_via_poynt(self, order, nonce, billing_postal_code=''): """Exchange a Poynt Collect nonce for a payment JWT and create a payment.token.""" provider = request.env['payment.provider'].sudo().search([ ('code', '=', 'poynt'), ('state', '!=', 'disabled'), ], limit=1) if not provider: raise UserError(_("Poynt payment provider is not configured.")) result = provider._poynt_tokenize_nonce(nonce) card_data = result.get('card', {}) card_id = card_data.get('id', '') or result.get('cardId', '') last_four = card_data.get('numberLast4', '') card_type = card_data.get('type', 'UNKNOWN') payment_jwt = result.get('paymentToken', '') avs_result = result.get('avsResult', {}) if avs_result and billing_postal_code: avs_match = avs_result.get('postalCodeMatch', '') if avs_match and avs_match not in ('MATCH', 'YES', 'Y'): _logger.warning( "AVS postal code mismatch for %s: expected=%s result=%s", order.name, billing_postal_code, avs_match, ) card_type_lower = card_type.lower() if card_type else '' PaymentMethod = request.env['payment.method'].sudo().with_context(active_test=False) payment_method = PaymentMethod.search( [('code', '=', card_type_lower)], limit=1, ) if not payment_method: payment_method = PaymentMethod.search( [('code', '=', 'card')], limit=1, ) if not payment_method: payment_method = PaymentMethod.search( [('code', 'in', ('visa', 'mastercard'))], limit=1, ) token = request.env['payment.token'].sudo().create({ 'provider_id': provider.id, 'payment_method_id': payment_method.id, 'partner_id': order.partner_id.id, 'provider_ref': card_id or nonce[:40], 'poynt_card_id': card_id, 'poynt_payment_token': payment_jwt, 'payment_details': f"{card_type} ending in {last_four}", }) return token # ================================================================= # Card Reauthorization # ================================================================= @http.route( '/rental/reauthorize//', type='http', auth='public', website=True, methods=['GET'], ) def rental_reauthorize_page(self, order_id, token, **kwargs): """Render the card reauthorization portal page.""" order = request.env['sale.order'].sudo().browse(order_id) if ( not order.exists() or order.rental_agreement_token != token or not order.is_rental_order ): return request.render( 'fusion_rental.cancellation_invalid_page', {'error': _("This link is invalid or has expired.")}, ) provider = request.env['payment.provider'].sudo().search([ ('code', '=', 'poynt'), ('state', '!=', 'disabled'), ], limit=1) return request.render( 'fusion_rental.card_reauthorization_page', { 'order': order, 'partner': order.partner_id, 'poynt_business_id': provider.poynt_business_id if provider else '', 'poynt_application_id': provider.poynt_application_id if provider else '', }, ) @http.route( '/rental/reauthorize///submit', type='json', auth='public', methods=['POST'], ) def rental_reauthorize_submit(self, order_id, token, **kwargs): """Process the card reauthorization submission.""" order = request.env['sale.order'].sudo().browse(order_id) if ( not order.exists() or order.rental_agreement_token != token or not order.is_rental_order ): return {'success': False, 'error': 'Invalid or expired link.'} cardholder_name = kwargs.get('cardholder_name', '').strip() nonce = kwargs.get('nonce', '').strip() billing_address = kwargs.get('billing_address', '').strip() billing_city = kwargs.get('billing_city', '').strip() billing_state = kwargs.get('billing_state', '').strip() billing_postal_code = kwargs.get('billing_postal_code', '').strip() if not cardholder_name: return {'success': False, 'error': 'Cardholder name is required.'} if not nonce: return {'success': False, 'error': 'Card authorization is required.'} if not billing_postal_code: return {'success': False, 'error': 'Billing postal/zip code is required.'} full_billing = billing_address if billing_city: full_billing += f", {billing_city}" if billing_state: full_billing += f", {billing_state}" try: payment_token = self._tokenize_nonce_via_poynt( order, nonce, billing_postal_code, ) except Exception as e: _logger.error( "Card reauthorization tokenization failed for %s: %s", order.name, e, ) return {'success': False, 'error': str(e)} old_token = order.rental_payment_token_id order.write({ 'rental_payment_token_id': payment_token.id, 'rental_billing_address': full_billing, 'rental_billing_postal_code': billing_postal_code, }) if old_token: try: old_token.active = False except Exception: pass order.message_post(body=_( "Card on file updated by %s. New card: %s", cardholder_name, payment_token.payment_details or 'Card', )) try: self._send_card_reauthorization_confirmation(order, payment_token, cardholder_name) except Exception as e: _logger.error( "Card reauth confirmation email failed for %s: %s", order.name, e, ) return { 'success': True, 'message': 'Card authorized successfully. Thank you!', } def _send_card_reauthorization_confirmation(self, order, token, cardholder_name): """Send confirmation email with card authorization details.""" template = request.env.ref( 'fusion_rental.mail_template_rental_card_reauth_confirmation', raise_if_not_found=False, ) if template: template.sudo().with_context( cardholder_name=cardholder_name, card_details=token.payment_details or 'Card', ).send_mail(order.id, force_send=True) # ================================================================= # Purchase Interest (from marketing email) # ================================================================= @http.route( '/rental/purchase-interest//', type='http', auth='public', website=True, methods=['GET'], ) def rental_purchase_interest(self, order_id, token, **kwargs): """Handle customer expressing purchase interest from marketing email.""" order = request.env['sale.order'].sudo().browse(order_id) if ( not order.exists() or order.rental_agreement_token != token ): return request.render( 'fusion_rental.cancellation_invalid_page', {'error': _("This link is invalid.")}, ) if not order.rental_purchase_interest: order.rental_purchase_interest = True order.activity_schedule( 'mail.mail_activity_data_todo', date_deadline=fields.Date.today(), summary=_("Customer interested in purchasing rental product"), note=_( "Customer %s expressed interest in purchasing the rental " "product from order %s. Please follow up.", order.partner_id.name, order.name, ), user_id=order.user_id.id or request.env.uid, ) order.message_post( body=_("Customer expressed purchase interest via marketing email."), ) return request.render( 'fusion_rental.purchase_interest_success_page', {'order': order, 'partner': order.partner_id}, )