1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/sale_subscription/tests/test_subscription_controller.py
2024-12-10 09:04:09 +07:00

480 lines
25 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
from datetime import date
from freezegun import freeze_time
from unittest.mock import patch
from odoo.addons.sale.models.sale_order import SaleOrder
from odoo.addons.sale_subscription.tests.common_sale_subscription import TestSubscriptionCommon
from odoo.addons.payment.tests.common import PaymentCommon
from odoo.addons.payment.tests.http_common import PaymentHttpCommon
from odoo.tests.common import new_test_user, tagged
from odoo.tools import mute_logger
from odoo import Command
from odoo import http
@tagged("post_install", "-at_install", "subscription_controller")
class TestSubscriptionController(PaymentHttpCommon, PaymentCommon, TestSubscriptionCommon):
def setUp(self):
super().setUp()
context_no_mail = {'no_reset_password': True, 'mail_create_nosubscribe': True, 'mail_create_nolog': True,}
SaleOrder = self.env['sale.order'].with_context(context_no_mail)
ProductTmpl = self.env['product.template'].with_context(context_no_mail)
self.user = new_test_user(self.env, "test_user_1", email="test_user_1@nowhere.com", tz="UTC")
self.other_user = new_test_user(self.env, "test_user_2", email="test_user_2@nowhere.com", password="P@ssw0rd!", tz="UTC")
self.partner = self.user.partner_id
# Test products
self.sub_product_tmpl = ProductTmpl.sudo().create({
'name': 'TestProduct',
'type': 'service',
'recurring_invoice': True,
'uom_id': self.env.ref('uom.product_uom_unit').id,
'product_subscription_pricing_ids': [Command.set((self.pricing_month + self.pricing_year).ids)],
})
self.subscription_tmpl = self.env['sale.order.template'].create({
'name': 'Subscription template without discount',
'duration_unit': 'year',
'is_unlimited': False,
'duration_value': 2,
'note': "This is the template description",
'plan_id': self.plan_month.id,
'sale_order_template_line_ids': [Command.create({
'name': "monthly",
'product_id': self.sub_product_tmpl.product_variant_ids.id,
'product_uom_qty': 1,
'product_uom_id': self.sub_product_tmpl.uom_id.id
}),
Command.create({
'name': "yearly",
'product_id': self.sub_product_tmpl.product_variant_ids.id,
'product_uom_qty': 1,
'product_uom_id': self.sub_product_tmpl.uom_id.id,
})
]
})
# Test Subscription
self.subscription = SaleOrder.create({
'name': 'TestSubscription',
'is_subscription': True,
'note': "original subscription description",
'partner_id': self.other_user.partner_id.id,
'pricelist_id': self.other_user.property_product_pricelist.id,
'sale_order_template_id': self.subscription_tmpl.id,
})
self.subscription._onchange_sale_order_template_id()
self.subscription.end_date = False # reset the end_date
self.subscription_tmpl.flush_recordset()
self.subscription.flush_recordset()
def test_close_contract(self):
""" Test subscription close """
with freeze_time("2021-11-18"):
self.authenticate(None, None)
self.subscription.plan_id.user_closable = True
self.subscription.action_confirm()
close_reason_id = self.env.ref('sale_subscription.close_reason_1')
data = {'access_token': self.subscription.access_token, 'csrf_token': http.Request.csrf_token(self),
'close_reason_id': close_reason_id.id, 'closing_text': "I am broke"}
url = "/my/subscriptions/%s/close" % self.subscription.id
res = self.url_open(url, allow_redirects=False, data=data)
self.assertEqual(res.status_code, 303)
self.env.invalidate_all()
self.assertEqual(self.subscription.subscription_state, '6_churn', 'The subscription should be closed.')
self.assertEqual(self.subscription.end_date, date(2021, 11, 18), 'The end date of the subscription should be updated.')
def test_prevents_assigning_not_owned_payment_tokens_to_subscriptions(self):
malicious_user_subscription = self.env['sale.order'].create({
'name': 'Free Subscription',
'partner_id': self.malicious_user.partner_id.id,
'sale_order_template_id': self.subscription_tmpl.id,
})
malicious_user_subscription._onchange_sale_order_template_id()
legit_user_subscription = self.env['sale.order'].create({
'name': 'Free Subscription',
'partner_id': self.legit_user.partner_id.id,
'sale_order_template_id': self.subscription_tmpl.id,
})
stolen_payment_method = self.env['payment.token'].create(
{'payment_details': 'Jimmy McNulty',
'partner_id': self.malicious_user.partner_id.id,
'provider_id': self.dummy_provider.id,
'payment_method_id': self.payment_method_id,
'provider_ref': 'Omar Little'})
legit_payment_method = self.env['payment.token'].create(
{'payment_details': 'Jimmy McNulty',
'partner_id': self.legit_user.partner_id.id,
'provider_id': self.dummy_provider.id,
'payment_method_id': self.payment_method_id,
'provider_ref': 'Legit ref'})
legit_user_subscription._portal_ensure_token()
malicious_user_subscription._portal_ensure_token()
# Payment Token exists/does not exists
# Payment Token is accessible to user/not accessible
# SO accessible to User/Accessible thanks to the token/Not Accessible (wrong token, no token)
# First we check a legit token assignation for a legit subscription.
self.authenticate('ness', 'nessnessness')
data = {'token_id': legit_payment_method.id,
'order_id': legit_user_subscription.id,
'access_token': legit_user_subscription.access_token
}
url = self._build_url("/my/subscriptions/assign_token/%s" % legit_user_subscription.id)
self.make_jsonrpc_request(url, data)
legit_user_subscription.invalidate_recordset()
self.assertEqual(legit_user_subscription.payment_token_id, legit_payment_method)
data = {'token_id': 9999999999999999, 'order_id': legit_user_subscription.id}
with self._assertNotFound():
self.make_jsonrpc_request(url, data)
legit_user_subscription.invalidate_recordset()
self.assertEqual(legit_user_subscription.payment_token_id, legit_payment_method, "The new token should be saved on the order.")
# Payment token is inacessible to user but the SO is OK
self.authenticate('al', 'alalalal')
data = {'token_id': legit_payment_method.id, 'order_id': malicious_user_subscription.id}
url = self._build_url("/my/subscriptions/assign_token/%s" % malicious_user_subscription.id)
with self._assertNotFound():
self.make_jsonrpc_request(url, data)
malicious_user_subscription.invalidate_recordset()
self.assertFalse(malicious_user_subscription.payment_token_id, "No token should be saved on the order.")
# The SO is not accessible but the token is mine
data = {'token_id': stolen_payment_method.id, 'order_id': legit_user_subscription.id}
self._build_url("/my/subscriptions/assign_token/%s" % legit_user_subscription.id)
with self._assertNotFound():
self.make_jsonrpc_request(url, data)
legit_user_subscription.invalidate_recordset()
self.assertEqual(legit_user_subscription.payment_token_id, legit_payment_method, "The token should not be updated")
def test_automatic_invoice_token(self):
self.original_prepare_invoice = self.subscription._prepare_invoice
self.mock_send_success_count = 0
with patch('odoo.addons.sale_subscription.models.sale_order.SaleOrder._do_payment', wraps=self._mock_subscription_do_payment),\
patch('odoo.addons.sale_subscription.models.sale_order.SaleOrder._send_success_mail', wraps=self._mock_subscription_send_success_mail):
self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'False')
subscription = self._portal_payment_controller_flow()
subscription.transaction_ids.unlink()
# set automatic invoice and restart
self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')
self._portal_payment_controller_flow()
def _portal_payment_controller_flow(self):
subscription = self.subscription.create({
'partner_id': self.partner.id,
'company_id': self.company.id,
'payment_token_id': self.payment_token.id,
'sale_order_template_id': self.subscription_tmpl.id,
})
subscription._onchange_sale_order_template_id()
subscription.state = 'sent'
subscription._portal_ensure_token()
signature = "R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=" # BASE64 of a simple image
data = {'order_id': subscription.id, 'access_token': subscription.access_token, 'signature': signature}
url = self._build_url("/my/orders/%s/accept" % subscription.id)
self.make_jsonrpc_request(url, data)
data = {
'provider_id': self.dummy_provider.id,
'payment_method_id': self.payment_method_id,
'token_id': None,
'order_id': subscription.id,
'access_token': subscription.access_token,
'amount': subscription.amount_total,
'flow': 'direct',
'tokenization_requested': True,
'landing_route': subscription.get_portal_url(),
}
url = self._build_url("/my/orders/%s/transaction" % subscription.id)
self.make_jsonrpc_request(url, data)
subscription.transaction_ids.provider_id.support_manual_capture = 'full_only'
subscription.transaction_ids._set_authorized()
subscription.invoice_ids.filtered(lambda am: am.state == 'draft')._post()
subscription.transaction_ids.token_id = self.payment_token.id
self.assertEqual(subscription.next_invoice_date, datetime.date.today())
self.assertEqual(subscription.state, 'sale')
subscription.transaction_ids._reconcile_after_done() # Create the payment
self.assertEqual(subscription.invoice_count, 1, "One invoice should be created")
# subscription has a payment_token_id, the invoice is created by the flow.
subscription.invoice_ids.invoice_line_ids.account_id.account_type = 'income'
subscription.invoice_ids.auto_post = 'at_date'
self.env.ref('account.ir_cron_auto_post_draft_entry').method_direct_trigger()
self.assertTrue(subscription.next_invoice_date > datetime.date.today(), "the next invoice date should be updated")
self.env['account.payment.register'] \
.with_context(active_model='account.move', active_ids=subscription.invoice_ids.ids) \
.create({
'currency_id': subscription.currency_id.id,
'amount': subscription.amount_total,
})._create_payments()
self.assertEqual(subscription.invoice_ids.mapped('state'), ['posted'])
self.assertTrue(subscription.invoice_ids.payment_state in ['paid', 'in_payment'])
subscription._cron_recurring_create_invoice()
invoices = subscription.invoice_ids.filtered(lambda am: am.state in ['draft', 'posted']) # avoid counting canceled invoices
self.assertEqual(len(invoices), 1, "Only one invoice should be created")
# test transaction flow when paying from the portal
self.assertEqual(len(subscription.transaction_ids), 1, "Only one transaction should be created")
first_transaction_id = subscription.transaction_ids
url = self._build_url("/my/subscriptions/%s/transaction" % subscription.id)
data = {'access_token': subscription.access_token,
'landing_route': subscription.get_portal_url(),
'provider_id': self.dummy_provider.id,
'payment_method_id': self.payment_method_id,
'token_id': False,
'flow': 'direct',
}
self.make_jsonrpc_request(url, data)
# the transaction is associated to the invoice in tx._reconcile_after_done()
invoice_transactions = subscription.invoice_ids.transaction_ids
self.assertEqual(len(invoice_transactions), 2, "Two transactions should be created. Calling /my/subscriptions/transaction/ creates a new one")
last_transaction_id = subscription.transaction_ids - first_transaction_id
self.assertEqual(len(subscription.transaction_ids), 2)
self.assertEqual(last_transaction_id.sale_order_ids, subscription)
last_transaction_id._set_done()
self.assertEqual(subscription.invoice_ids.sorted('id').mapped('state'), ['posted', 'draft'])
subscription.invoice_ids.filtered(lambda am: am.state == 'draft')._post()
subscription.transaction_ids._reconcile_after_done() # Create the payment
# subscription has a payment_token_id, the invoice is created by the flow.
subscription.invoice_ids.invoice_line_ids.account_id.account_type = 'asset_cash'
subscription.invoice_ids.auto_post = 'at_date'
subscription.invoice_ids.filtered(lambda am: am.state == 'draft')._post()
self.env['account.payment.register'] \
.with_context(active_model='account.move', active_ids=subscription.invoice_ids.ids) \
.create({
'currency_id': subscription.currency_id.id,
'amount': subscription.amount_total,
})._create_payments()
self.assertFalse(set(subscription.invoice_ids.mapped('payment_state')) & {'not_paid', 'partial'},
"All invoices should be in paid or in_payment status")
return subscription
def test_controller_transaction_refund(self):
self.original_prepare_invoice = self.subscription._prepare_invoice
self.mock_send_success_count = 0
self.pricing_month.price = 10
subscription = self.subscription.create({
'partner_id': self.partner.id,
'company_id': self.company.id,
'payment_token_id': self.payment_token.id,
'sale_order_template_id': self.subscription_tmpl.id,
})
subscription._onchange_sale_order_template_id()
subscription.order_line.product_uom_qty = 2
subscription.action_confirm()
invoice = subscription._create_invoices()
invoice._post()
self.assertEqual(invoice.amount_total, 46)
# partial refund
refund_wizard = self.env['account.move.reversal'].with_context(
active_model="account.move",
active_ids=invoice.ids).create({
'reason': 'Test refund',
'journal_id': invoice.journal_id.id,
})
res = refund_wizard.reverse_moves()
refund_move = self.env['account.move'].browse(res['res_id'])
refund_move.invoice_line_ids.quantity = 1
refund_move._post()
self.assertEqual(refund_move.amount_total, 23, "The refund is half the invoice")
url = self._build_url("/my/subscriptions/%s/transaction/" % subscription.id)
data = {'access_token': subscription.access_token,
'landing_route': subscription.get_portal_url(),
'provider_id': self.dummy_provider.id,
'payment_method_id': self.payment_method_id,
'token_id': False,
'flow': 'direct',
}
self.make_jsonrpc_request(url, data)
invoice_transactions = subscription.invoice_ids.transaction_ids
# the amount should be equal to the last
self.assertEqual(invoice_transactions.amount, subscription.amount_total,
"The last transaction should be equal to the total")
def test_portal_partial_payment(self):
with patch('odoo.addons.sale_subscription.models.sale_order.SaleOrder._do_payment',
wraps=self._mock_subscription_do_payment), \
patch('odoo.addons.sale_subscription.models.sale_order.SaleOrder._send_success_mail',
wraps=self._mock_subscription_send_success_mail):
self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'False')
with freeze_time("2024-01-22"):
subscription = self.subscription.create({
'partner_id': self.partner.id,
'company_id': self.company.id,
'sale_order_template_id': self.subscription_tmpl.id,
})
self.pricing_month.price = 100
subscription._onchange_sale_order_template_id()
subscription.state = 'sent'
subscription._portal_ensure_token()
# test customized /payment/pay route with sale_order_id param
# partial amount specified
self.amount = subscription.amount_total / 2.0 # self.amount is used to create the right transaction
pay_route_values = self._prepare_pay_values(
amount=self.amount,
currency=subscription.currency_id,
partner=subscription.partner_id,
)
pay_route_values['sale_order_id'] = subscription.id
tx_context = self._get_portal_pay_context(**pay_route_values)
tx_route_values = {
'provider_id': self.provider.id,
'payment_method_id': self.payment_method_id,
'token_id': None,
'amount': tx_context['amount'],
'flow': 'direct',
'tokenization_requested': False,
'landing_route': '/my/subscriptions',
'access_token': tx_context['access_token'],
}
with mute_logger('odoo.addons.payment.models.payment_transaction'):
processing_values = self._get_processing_values(
tx_route=tx_context['transaction_route'], **tx_route_values
)
tx_sudo = self._get_tx(processing_values['reference'])
# make sure to have a token on the transaction. it is needed to test the confirmation flow
tx_sudo.token_id = self.payment_token.id
self.assertEqual(tx_sudo.sale_order_ids, subscription)
# self.assertEqual(tx_sudo.amount, amount)
self.assertEqual(tx_sudo.sale_order_ids.transaction_ids, tx_sudo)
tx_sudo._set_done()
with mute_logger('odoo.addons.sale.models.payment_transaction'):
tx_sudo._finalize_post_processing()
self.assertEqual(subscription.state, 'sent') # Only a partial amount was paid
subscription.action_confirm()
self.assertEqual(subscription.next_invoice_date, datetime.date.today())
self.assertEqual(subscription.state, 'sale')
self.assertEqual(subscription.invoice_count, 0, "No invoice should be created")
self.assertFalse(subscription.payment_token_id, "No token should be saved")
# Renew subscription and set payment amount as half of the total amount (partial).
subscription._create_recurring_invoice()
action = subscription.prepare_renewal_order()
renewal_so = self.env['sale.order'].browse(action['res_id'])
self.amount = renewal_so.amount_total / 2.0
# Prepare renewal subscription's payment values.
pay_route_values = self._prepare_pay_values(
amount=self.amount,
currency=renewal_so.currency_id,
partner=renewal_so.partner_id,
)
pay_route_values['sale_order_id'] = renewal_so.id
tx_context = self._get_portal_pay_context(**pay_route_values)
tx_route_values = {
'provider_id': self.provider.id,
'payment_method_id': self.payment_method_id,
'token_id': None,
'amount': tx_context['amount'],
'flow': 'direct',
'tokenization_requested': False,
'landing_route': '/my/subscriptions',
'access_token': tx_context['access_token'],
}
with mute_logger('odoo.addons.payment.models.payment_transaction'):
processing_values = self._get_processing_values(tx_route=tx_context['transaction_route'], **tx_route_values)
# Make sure to have a token on the transaction, it is needed to test the confirmation flow.
tx_sudo = self._get_tx(processing_values['reference'])
tx_sudo.token_id = self.payment_token.id
self.assertEqual(tx_sudo.sale_order_ids, renewal_so)
self.assertEqual(tx_sudo.sale_order_ids.transaction_ids, tx_sudo)
tx_sudo._set_done()
with mute_logger('odoo.addons.sale.models.payment_transaction'):
tx_sudo._finalize_post_processing()
# Confirm renewal. Assert that no token was saved, renewal was sent and only one invoice was registered.
renewal_so.action_confirm()
self.assertFalse(renewal_so.payment_token_id, "No token should be saved")
self.assertEqual(renewal_so.state, 'sale')
self.assertEqual(renewal_so.invoice_count, 1, "Only one invoice from previous subscription should be registered")
self.assertEqual(renewal_so.next_invoice_date, datetime.date.today() + datetime.timedelta(days=31))
def test_portal_quote_document(self):
product_document = self.env['product.document'].create({
'name': 'doc.txt',
'active': True,
'datas': 'TXkgYXR0YWNobWVudA==',
'res_model': 'product.product',
'res_id': self.sub_product_tmpl.product_variant_ids.id,
'attached_on': 'sale_order',
})
self.subscription.action_confirm()
response = self.url_open(
self.subscription.get_portal_url('/document/' + str(product_document.id))
)
self.assertEqual(response.status_code, 200)
self.assertEqual("My attachment", response.text)
def test_payment_confirmation_email(self):
"""Check that payment confirmation emails aren't sent for validation transactions."""
def process_notification_data(data):
tx = self.env['payment.transaction'].search(
[('reference', '=', data['reference'])],
limit=1,
)
tx.token_id = tx.tokenize and self.env['payment.token'].create({
'provider_id': self.provider.id,
'payment_method_id': self.payment_method_id,
'partner_id': self.subscription.partner_id.id,
'provider_ref': 'test123',
})
tx._set_done()
self.portal_user.email = 'chell@aperture.com'
self.subscription.partner_id = self.portal_partner
self.subscription.action_confirm()
self.subscription._create_invoices()
with patch.object(SaleOrder, '_send_order_notification_mail') as notification_mail_mock:
subscription_tx_url = f'/my/subscriptions/{self.subscription.id}/transaction'
base_tx_route_values = {
'access_token': None,
'order_id': self.subscription.id,
'amount': 0.0,
'provider_id': self.provider.id,
'payment_method_id': self.payment_method_id,
'token_id': None,
'flow': 'direct',
'is_validation': False,
'tokenization_requested': False,
'landing_route': self.subscription.get_portal_url(),
}
# Log in and save a payment method for a subscription
self.authenticate(self.portal_user.login, self.portal_user.login)
tx_response = self.make_jsonrpc_request(subscription_tx_url, {
**base_tx_route_values,
'is_validation': True,
'tokenization_requested': True,
})
process_notification_data(tx_response)
self.make_jsonrpc_request('/payment/status/poll', {})
self.assertFalse(
notification_mail_mock.call_count,
"Simply setting a payment token shouldn't send a payment succeeded email",
)
# Use saved payment method to pay subscription
tx_response = self.make_jsonrpc_request(subscription_tx_url, {
**base_tx_route_values,
'amount': self.subscription.amount_total,
'token_id': self.subscription.payment_token_id.id,
'flow': 'token',
})
process_notification_data(tx_response)
self.make_jsonrpc_request('/payment/status/poll', {})
self.assertEqual(
notification_mail_mock.call_count,
1,
"Paying a subscription should send one payment succeeded email",
)