[ADD] website_sale_aplicoop: online payment per group order
Members can now pay their eskaera at checkout, through the standard Odoo payment machinery. Enabled per group order with a new `online_payment` boolean, off by default: an order without it behaves exactly as before, members save a draft and the cutoff cron confirms them in bulk. The flow mirrors website_sale's: the checkout button becomes "Confirm and pay", saving the cart redirects to a new /eskaera/<slug>/payment step that renders `payment.form` from `sale`'s `_get_payment_values`, and the standard /my/orders/<id>/transaction route takes it from there. This addon ships no provider and configures none; the co-op publishes whichever it wants. `website_sale`'s `_get_shop_payment_values` is deliberately not reused: it runs `_get_shop_payment_errors`, which blocks on shippable products without a delivery method — exactly an eskaera order, collected at the co-op with no carrier. For the same reason the transaction route stays the portal one, which does not call `_check_cart_is_ready_to_be_paid()`. Payment confirms the order, which has three consequences handled here: * `payment.transaction._check_amount_and_confirm_order` now confirms group orders with `from_orderpoint=True`, the way the cutoff cron already does. Without it a product with a broken replenishment route raises inside `_post_process`, and `/payment/status/poll` rolls back and re-raises: the member sees a payment error over a `done` transaction and the retry cron fails forever. * `_confirm_linked_sale_orders` also sweeps the cycle's already confirmed orders into the picking batch, scoped by `pickup_date`. Its early return on "no drafts" ran before any batching, so a fully prepaid cycle produced no batch at all. `_cron_batch_paid_orders_of_closed_cycles` covers the same hole for cycles closed by hand. * A duplicate-order guard answers 409 on save-order, add-to-cart and load-draft, and shows a notice on the shop, so a member whose order is already placed cannot build and pay for a second one. The payment policy lives in the model rather than the controller: there are three sale.order creation paths and two are live, so `_compute_require_payment` and `_compute_prepayment_percent` are extended instead of patching five vals dicts. Orders are also created under the group order's company, which is what filters the payment providers. Along the way: eskaera drafts were invisible in /my/orders. The portal rule is `message_partner_ids child_of` and sale.order only subscribes the customer on send or confirm, never on a draft create, so the `_prepare_orders_domain` override that includes drafts never had any effect. Fixed with an explicit `message_subscribe`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
a67181ab42
commit
6ba554c91b
21 changed files with 1993 additions and 37 deletions
|
|
@ -18,3 +18,4 @@ from . import test_cron_picking_batch # noqa: F401
|
|||
from . import test_group_order_status_endpoint # noqa: F401
|
||||
from . import test_home_delivery # noqa: F401
|
||||
from . import test_forecasted_stock # noqa: F401
|
||||
from . import test_online_payment # noqa: F401
|
||||
|
|
|
|||
568
website_sale_aplicoop/tests/test_online_payment.py
Normal file
568
website_sale_aplicoop/tests/test_online_payment.py
Normal file
|
|
@ -0,0 +1,568 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
|
||||
from datetime import timedelta
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo import fields
|
||||
from odoo.tests.common import HttpCase
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.tests.common import tagged
|
||||
|
||||
from odoo.addons.website_sale_aplicoop.controllers import (
|
||||
website_sale_validators as validators,
|
||||
)
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install", "eskaera_online_payment")
|
||||
class TestOnlinePayment(TransactionCase):
|
||||
"""Online payment for group orders: policy, guards and batching."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.consumer_group = cls.env["res.partner"].create(
|
||||
{
|
||||
"name": "Payment Consumer Group",
|
||||
"is_company": True,
|
||||
"is_group": True,
|
||||
}
|
||||
)
|
||||
cls.member = cls.env["res.partner"].create(
|
||||
{
|
||||
"name": "Paying Member",
|
||||
"email": "paying.member@test.com",
|
||||
"parent_id": cls.consumer_group.id,
|
||||
}
|
||||
)
|
||||
cls.other_member = cls.env["res.partner"].create(
|
||||
{
|
||||
"name": "Other Member",
|
||||
"email": "other.member@test.com",
|
||||
"parent_id": cls.consumer_group.id,
|
||||
}
|
||||
)
|
||||
cls.product = cls.env["product.product"].create(
|
||||
{
|
||||
"name": "Payable Product",
|
||||
"is_storable": True,
|
||||
"list_price": 10.0,
|
||||
}
|
||||
)
|
||||
# The validator helpers only ever reach for `request.env`, so a
|
||||
# namespace stands in for the HTTP request outside a web context.
|
||||
cls.fake_request = SimpleNamespace(env=cls.env)
|
||||
|
||||
# === Helpers ===
|
||||
|
||||
def _create_group_order(self, online_payment=True, cutoff_in_past=False):
|
||||
"""One-time group order whose cycle ends in the past or the future.
|
||||
|
||||
One-time orders derive `cutoff_date` from `end_date`, so the cycle is
|
||||
steered here through `end_date`.
|
||||
"""
|
||||
today = fields.Date.today()
|
||||
end_date = (
|
||||
today - timedelta(days=1) if cutoff_in_past else today + timedelta(days=2)
|
||||
)
|
||||
return self.env["group.order"].create(
|
||||
{
|
||||
"name": "Payment Group Order",
|
||||
"group_ids": [(6, 0, [self.consumer_group.id])],
|
||||
"period": "once",
|
||||
"pickup_day": "2", # Wednesday
|
||||
"state": "open",
|
||||
"end_date": end_date,
|
||||
"online_payment": online_payment,
|
||||
}
|
||||
)
|
||||
|
||||
def _create_sale_order(self, group_order, partner=None, pickup_date=None):
|
||||
return self.env["sale.order"].create(
|
||||
{
|
||||
"partner_id": (partner or self.member).id,
|
||||
"group_order_id": group_order.id,
|
||||
"consumer_group_id": self.consumer_group.id,
|
||||
"pickup_date": pickup_date or group_order.pickup_date,
|
||||
"order_line": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": self.product.id,
|
||||
"product_uom_qty": 1,
|
||||
"price_unit": 10.0,
|
||||
},
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
def _create_done_transaction(self, sale_order):
|
||||
"""A `done` transaction covering the order's full amount.
|
||||
|
||||
`payment.method` records ship archived until a provider module is
|
||||
installed, so the lookup has to ignore the active flag: the test only
|
||||
needs a well-formed transaction, not a usable payment route.
|
||||
"""
|
||||
provider = self.env["payment.provider"].search([], limit=1)
|
||||
method = (
|
||||
self.env["payment.method"]
|
||||
.with_context(active_test=False)
|
||||
.search([("primary_payment_method_id", "=", False)], limit=1)
|
||||
)
|
||||
transaction = self.env["payment.transaction"].create(
|
||||
{
|
||||
"provider_id": provider.id,
|
||||
"payment_method_id": method.id,
|
||||
"reference": f"TEST-{sale_order.id}",
|
||||
"amount": sale_order.amount_total,
|
||||
"currency_id": sale_order.currency_id.id,
|
||||
"partner_id": sale_order.partner_id.id,
|
||||
"sale_order_ids": [(6, 0, sale_order.ids)],
|
||||
}
|
||||
)
|
||||
transaction.write({"state": "done"})
|
||||
return transaction
|
||||
|
||||
# === Payment policy on the sale order ===
|
||||
|
||||
def test_require_payment_follows_group_order(self):
|
||||
"""A group order with online payment makes its orders payable."""
|
||||
group_order = self._create_group_order(online_payment=True)
|
||||
sale_order = self._create_sale_order(group_order)
|
||||
|
||||
self.assertTrue(sale_order.require_payment)
|
||||
self.assertEqual(sale_order.prepayment_percent, 1.0)
|
||||
self.assertTrue(
|
||||
sale_order._has_to_be_paid(),
|
||||
"A draft order of a paying cycle must be payable",
|
||||
)
|
||||
|
||||
def test_require_payment_off_without_online_payment(self):
|
||||
"""Without the flag nothing changes: no payment is required."""
|
||||
group_order = self._create_group_order(online_payment=False)
|
||||
sale_order = self._create_sale_order(group_order)
|
||||
|
||||
self.assertFalse(sale_order.require_payment)
|
||||
self.assertFalse(sale_order._has_to_be_paid())
|
||||
|
||||
def test_toggling_group_order_clears_require_payment(self):
|
||||
"""Turning the flag off mid-cycle must free the existing drafts."""
|
||||
group_order = self._create_group_order(online_payment=True)
|
||||
sale_order = self._create_sale_order(group_order)
|
||||
self.assertTrue(sale_order.require_payment)
|
||||
|
||||
group_order.online_payment = False
|
||||
sale_order.invalidate_recordset()
|
||||
|
||||
self.assertFalse(
|
||||
sale_order.require_payment,
|
||||
"An existing draft must stop requiring payment when the group "
|
||||
"order stops offering it",
|
||||
)
|
||||
|
||||
def test_non_group_orders_keep_company_default(self):
|
||||
"""Orders outside a group order are left alone."""
|
||||
plain_order = self.env["sale.order"].create({"partner_id": self.member.id})
|
||||
self.assertEqual(
|
||||
plain_order.require_payment,
|
||||
plain_order.company_id.portal_confirmation_pay,
|
||||
)
|
||||
|
||||
# === Confirmation through payment ===
|
||||
|
||||
def test_payment_confirms_with_from_orderpoint(self):
|
||||
"""Paying must confirm the way the cutoff cron does.
|
||||
|
||||
Without `from_orderpoint`, a product with a broken replenishment route
|
||||
raises during post-processing, `/payment/status/poll` rolls the whole
|
||||
thing back and the member sees an error over a `done` transaction.
|
||||
"""
|
||||
group_order = self._create_group_order(online_payment=True)
|
||||
sale_order = self._create_sale_order(group_order)
|
||||
transaction = self._create_done_transaction(sale_order)
|
||||
|
||||
captured = {}
|
||||
|
||||
def _fake_confirm(order_self):
|
||||
captured["from_orderpoint"] = order_self.env.context.get("from_orderpoint")
|
||||
return True
|
||||
|
||||
with patch.object(
|
||||
type(self.env["sale.order"]), "action_confirm", _fake_confirm
|
||||
):
|
||||
transaction._check_amount_and_confirm_order()
|
||||
|
||||
self.assertTrue(
|
||||
captured.get("from_orderpoint"),
|
||||
"Group order confirmations triggered by payment must carry "
|
||||
"from_orderpoint=True",
|
||||
)
|
||||
|
||||
def test_payment_confirms_the_order(self):
|
||||
"""The standard machinery confirms the order once paid."""
|
||||
group_order = self._create_group_order(online_payment=True)
|
||||
sale_order = self._create_sale_order(group_order)
|
||||
transaction = self._create_done_transaction(sale_order)
|
||||
|
||||
transaction._check_amount_and_confirm_order()
|
||||
sale_order.invalidate_recordset()
|
||||
|
||||
self.assertEqual(sale_order.state, "sale")
|
||||
|
||||
def test_plain_orders_confirm_without_from_orderpoint(self):
|
||||
"""Orders unrelated to a group order keep the core behaviour."""
|
||||
plain_order = self.env["sale.order"].create(
|
||||
{
|
||||
"partner_id": self.member.id,
|
||||
"order_line": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": self.product.id,
|
||||
"product_uom_qty": 1,
|
||||
"price_unit": 10.0,
|
||||
},
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
transaction = self._create_done_transaction(plain_order)
|
||||
|
||||
captured = {}
|
||||
|
||||
def _fake_confirm(order_self):
|
||||
captured["from_orderpoint"] = order_self.env.context.get("from_orderpoint")
|
||||
return True
|
||||
|
||||
with patch.object(
|
||||
type(self.env["sale.order"]), "action_confirm", _fake_confirm
|
||||
):
|
||||
transaction._check_amount_and_confirm_order()
|
||||
|
||||
self.assertFalse(captured.get("from_orderpoint"))
|
||||
|
||||
# === Cycle lookup helpers ===
|
||||
|
||||
def test_draft_lookup_ignores_placed_orders(self):
|
||||
"""`_find_recent_draft_order` must never return a placed order.
|
||||
|
||||
`/eskaera/clear-cart` cancels whatever this returns, so widening it
|
||||
would cancel orders that are already paid for.
|
||||
"""
|
||||
group_order = self._create_group_order(online_payment=True)
|
||||
sale_order = self._create_sale_order(group_order)
|
||||
sale_order.action_confirm()
|
||||
|
||||
found = validators._find_recent_draft_order(
|
||||
None, self.member.id, group_order, request_obj=self.fake_request
|
||||
)
|
||||
self.assertFalse(found)
|
||||
|
||||
def test_placed_lookup_finds_confirmed_order(self):
|
||||
"""The duplicate guard sees the member's confirmed order."""
|
||||
group_order = self._create_group_order(online_payment=True)
|
||||
sale_order = self._create_sale_order(group_order)
|
||||
sale_order.action_confirm()
|
||||
|
||||
found = validators._find_placed_cycle_order(
|
||||
None, self.member.id, group_order, request_obj=self.fake_request
|
||||
)
|
||||
self.assertEqual(found, sale_order)
|
||||
|
||||
def test_placed_lookup_survives_orders_created_after_cutoff(self):
|
||||
"""Nothing blocks ordering between the cutoff and the cron run.
|
||||
|
||||
The draft window caps `create_date` at the cutoff date; the placed
|
||||
lookup must not, or an order paid in that gap would slip past the
|
||||
guard and let the member order twice.
|
||||
"""
|
||||
group_order = self._create_group_order(online_payment=True, cutoff_in_past=True)
|
||||
sale_order = self._create_sale_order(group_order)
|
||||
sale_order.action_confirm()
|
||||
|
||||
found = validators._find_placed_cycle_order(
|
||||
None, self.member.id, group_order, request_obj=self.fake_request
|
||||
)
|
||||
self.assertEqual(found, sale_order)
|
||||
|
||||
def test_placed_lookup_ignores_other_cycles(self):
|
||||
"""An order frozen on another pickup date belongs to another cycle."""
|
||||
group_order = self._create_group_order(online_payment=True)
|
||||
sale_order = self._create_sale_order(
|
||||
group_order, pickup_date=group_order.pickup_date - timedelta(days=7)
|
||||
)
|
||||
sale_order.action_confirm()
|
||||
|
||||
found = validators._find_placed_cycle_order(
|
||||
None, self.member.id, group_order, request_obj=self.fake_request
|
||||
)
|
||||
self.assertFalse(found)
|
||||
|
||||
# === Batching at cutoff ===
|
||||
|
||||
def test_cron_batches_a_fully_prepaid_cycle(self):
|
||||
"""A cycle where everybody paid early still gets its batch.
|
||||
|
||||
The confirmation loop used to bail out when it found no draft, which
|
||||
with online payment is the normal case: every order is confirmed the
|
||||
moment its transaction completes.
|
||||
"""
|
||||
group_order = self._create_group_order(online_payment=True, cutoff_in_past=True)
|
||||
sale_order = self._create_sale_order(group_order)
|
||||
sale_order.action_confirm()
|
||||
|
||||
self.assertFalse(
|
||||
self.env["sale.order"].search(
|
||||
[("group_order_id", "=", group_order.id), ("state", "=", "draft")]
|
||||
),
|
||||
"This cycle must have no drafts left for the test to mean anything",
|
||||
)
|
||||
|
||||
group_order._confirm_linked_sale_orders()
|
||||
|
||||
self.assertTrue(
|
||||
sale_order.picking_ids.batch_id,
|
||||
"The picking of an order paid before the cutoff must still be "
|
||||
"batched by the cron",
|
||||
)
|
||||
|
||||
def test_cron_batches_paid_and_draft_orders_together(self):
|
||||
"""Paid and cron-confirmed orders share one batch per picking type."""
|
||||
group_order = self._create_group_order(online_payment=True, cutoff_in_past=True)
|
||||
paid_order = self._create_sale_order(group_order, partner=self.member)
|
||||
paid_order.action_confirm()
|
||||
draft_order = self._create_sale_order(group_order, partner=self.other_member)
|
||||
|
||||
group_order._confirm_linked_sale_orders()
|
||||
draft_order.invalidate_recordset()
|
||||
|
||||
self.assertEqual(draft_order.state, "sale")
|
||||
batches = paid_order.picking_ids.batch_id | draft_order.picking_ids.batch_id
|
||||
self.assertEqual(
|
||||
len(batches),
|
||||
1,
|
||||
"Both orders belong to the same cycle and picking type, so they "
|
||||
"must land in a single batch",
|
||||
)
|
||||
|
||||
def test_cron_ignores_paid_orders_of_previous_cycles(self):
|
||||
"""A previous cycle's order must not be swept into this batch."""
|
||||
group_order = self._create_group_order(online_payment=True, cutoff_in_past=True)
|
||||
stale_order = self._create_sale_order(
|
||||
group_order, pickup_date=group_order.pickup_date - timedelta(days=7)
|
||||
)
|
||||
stale_order.action_confirm()
|
||||
stale_order.picking_ids.batch_id = False
|
||||
|
||||
group_order._confirm_linked_sale_orders()
|
||||
|
||||
self.assertFalse(
|
||||
stale_order.picking_ids.batch_id,
|
||||
"An order frozen on a previous pickup date is not part of this "
|
||||
"cycle and must be left out of its batch",
|
||||
)
|
||||
|
||||
def test_closed_cycle_still_batches_paid_orders(self):
|
||||
"""Closing a group order by hand must not strand a paid order."""
|
||||
group_order = self._create_group_order(online_payment=True, cutoff_in_past=True)
|
||||
sale_order = self._create_sale_order(group_order)
|
||||
sale_order.action_confirm()
|
||||
group_order.action_close()
|
||||
|
||||
self.env["group.order"]._cron_batch_paid_orders_of_closed_cycles()
|
||||
|
||||
self.assertTrue(
|
||||
sale_order.picking_ids.batch_id,
|
||||
"A paid order of a manually closed cycle must still be batched",
|
||||
)
|
||||
|
||||
def test_closed_cycle_leaves_drafts_alone(self):
|
||||
"""Closing a cycle by hand is how a co-op calls it off."""
|
||||
group_order = self._create_group_order(online_payment=True, cutoff_in_past=True)
|
||||
draft_order = self._create_sale_order(group_order)
|
||||
group_order.action_close()
|
||||
|
||||
self.env["group.order"]._cron_batch_paid_orders_of_closed_cycles()
|
||||
draft_order.invalidate_recordset()
|
||||
|
||||
self.assertEqual(
|
||||
draft_order.state,
|
||||
"draft",
|
||||
"The closed-cycle sweep must only batch, never confirm",
|
||||
)
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install", "eskaera_online_payment")
|
||||
class TestOnlinePaymentRoutes(HttpCase):
|
||||
"""The payment step and its landing page, over HTTP."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.group = self.env["res.partner"].create(
|
||||
{
|
||||
"name": "Payment Routes Group",
|
||||
"is_company": True,
|
||||
"is_group": True,
|
||||
"email": "payment-routes-group@test.com",
|
||||
}
|
||||
)
|
||||
self.member_partner = self.env["res.partner"].create(
|
||||
{"name": "Payment Routes Member", "email": "payment-routes@test.com"}
|
||||
)
|
||||
self.group.member_ids = [(4, self.member_partner.id)]
|
||||
|
||||
login = "portal.payment@test.com"
|
||||
self.portal_user = self.env["res.users"].create(
|
||||
{
|
||||
"name": "Portal Payment User",
|
||||
"login": login,
|
||||
"password": login,
|
||||
"partner_id": self.member_partner.id,
|
||||
"groups_id": [(4, self.env.ref("base.group_portal").id)],
|
||||
}
|
||||
)
|
||||
|
||||
self.product = self.env["product.product"].create(
|
||||
{"name": "Route Product", "is_storable": True, "list_price": 10.0}
|
||||
)
|
||||
|
||||
start_date = fields.Date.today()
|
||||
self.group_order = self.env["group.order"].create(
|
||||
{
|
||||
"name": "Payment Routes Order",
|
||||
"group_ids": [(6, 0, [self.group.id])],
|
||||
"type": "regular",
|
||||
"start_date": start_date,
|
||||
"end_date": start_date + timedelta(days=7),
|
||||
"period": "weekly",
|
||||
"pickup_day": "3",
|
||||
"cutoff_day": "0",
|
||||
"online_payment": True,
|
||||
}
|
||||
)
|
||||
self.group_order.action_open()
|
||||
|
||||
def _create_draft(self):
|
||||
return self.env["sale.order"].create(
|
||||
{
|
||||
"partner_id": self.member_partner.id,
|
||||
"group_order_id": self.group_order.id,
|
||||
"consumer_group_id": self.group.id,
|
||||
"pickup_date": self.group_order.pickup_date,
|
||||
"order_line": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": self.product.id,
|
||||
"product_uom_qty": 1,
|
||||
"price_unit": 10.0,
|
||||
},
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
def _slug_url(self, suffix=""):
|
||||
return f"/eskaera/{self.group_order.slug}{suffix}"
|
||||
|
||||
def test_payment_page_renders(self):
|
||||
"""The payment step renders for a member with a draft in the cycle."""
|
||||
self._create_draft()
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
|
||||
response = self.url_open(self._slug_url("/payment"), allow_redirects=True)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(
|
||||
'data-name="Eskaera Payment"',
|
||||
response.text,
|
||||
"The payment step should render its own page, not redirect away",
|
||||
)
|
||||
|
||||
def test_payment_page_needs_a_draft(self):
|
||||
"""With nothing in the cart there is nothing to pay for."""
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
|
||||
response = self.url_open(self._slug_url("/payment"), allow_redirects=False)
|
||||
|
||||
self.assertEqual(response.status_code, 303)
|
||||
self.assertTrue(response.headers["Location"].endswith("/checkout"))
|
||||
|
||||
def test_payment_page_off_without_online_payment(self):
|
||||
"""The step does not exist for a group order that takes no payments."""
|
||||
self.group_order.online_payment = False
|
||||
self._create_draft()
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
|
||||
response = self.url_open(self._slug_url("/payment"), allow_redirects=False)
|
||||
|
||||
self.assertEqual(response.status_code, 303)
|
||||
self.assertTrue(response.headers["Location"].endswith("/checkout"))
|
||||
|
||||
def test_checkout_offers_payment(self):
|
||||
"""The checkout button turns into the 'confirm and pay' variant.
|
||||
|
||||
Asserted on `data-tooltip-key` rather than the label: the website runs
|
||||
in whatever language the visitor picked, and the label is translated.
|
||||
"""
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
|
||||
response = self.url_open(self._slug_url("/checkout"), allow_redirects=True)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('data-tooltip-key="confirm_and_pay"', response.text)
|
||||
|
||||
def test_checkout_keeps_save_draft_without_online_payment(self):
|
||||
"""With the flag off the checkout is exactly what it was."""
|
||||
self.group_order.online_payment = False
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
|
||||
response = self.url_open(self._slug_url("/checkout"), allow_redirects=True)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('data-tooltip-key="save_draft"', response.text)
|
||||
self.assertNotIn('data-tooltip-key="confirm_and_pay"', response.text)
|
||||
|
||||
def test_confirmation_page_renders_for_the_owner(self):
|
||||
"""The landing page reports the order back to the member."""
|
||||
order = self._create_draft()
|
||||
order.action_confirm()
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
|
||||
response = self.url_open(
|
||||
self._slug_url(f"/payment/confirmation/{order.id}"), allow_redirects=True
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(order.name, response.text)
|
||||
|
||||
def test_confirmation_page_rejects_other_partners(self):
|
||||
"""Nobody gets to read someone else's order through this page."""
|
||||
other_partner = self.env["res.partner"].create({"name": "Somebody Else"})
|
||||
order = self._create_draft()
|
||||
order.partner_id = other_partner
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
|
||||
response = self.url_open(
|
||||
self._slug_url(f"/payment/confirmation/{order.id}"), allow_redirects=False
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 303)
|
||||
self.assertTrue(response.headers["Location"].endswith("/eskaera"))
|
||||
|
||||
def test_checkout_redirects_once_the_order_is_placed(self):
|
||||
"""A member who already paid cannot build a second order."""
|
||||
order = self._create_draft()
|
||||
order.action_confirm()
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
|
||||
response = self.url_open(self._slug_url("/checkout"), allow_redirects=False)
|
||||
|
||||
self.assertEqual(response.status_code, 303)
|
||||
self.assertIn(f"/payment/confirmation/{order.id}", response.headers["Location"])
|
||||
Loading…
Add table
Add a link
Reference in a new issue