[IMP] website_sale_aplicoop: pay on the checkout page, not a step later
The separate /eskaera/<slug>/payment step is gone. Members review the summary, choose home delivery and pick a payment method on the checkout, in one screen; the old URL redirects there so bookmarks and sessions that were mid-flow do not hit a 404. The checkout now renders the member's draft sale.order instead of the localStorage cart. That is what fixes the products appearing "out of nowhere" between the two pages: the summary was a snapshot of localStorage taken at page load, and `_autoLoadDraftOnInit` then pulled the draft back into localStorage without re-rendering. Deleting a product in the shop removed it from the cart but left the line on the draft, so the autoload resurrected it, the confirm button sent it back, and it only became visible one page later. The checkout no longer auto-loads the draft — it renders it, and what it shows is what the payment form charges. "Proceed to Checkout" pushes the cart to that draft before navigating. Saving is idempotent: `_merge_or_replace_draft` reuses the cycle's draft and, through the new `_draft_matches_lines`, rewrites `order_line` only when the lines actually differ — replacing them unlinks and recreates every one of them, which is pure churn when nothing changed. The home delivery checkbox goes through the new /eskaera/set-home-delivery so the delivery line moves on the order itself. Writing only to localStorage would have changed the summary and left the amount alone, which with online payment on is the amount being charged. Also fixes the confirmation notice nobody ever saw: saving answered with the payment step URL and the frontend followed it immediately, destroying the toast in the same tick. Saving no longer navigates; the caller decides whether it is staying or moving on. Along the way: checkout_labels.js and the eskaera_checkout_summary / eskaera_payment templates are removed, superseded by the server-rendered summary and checkout, and the stale sessionStorage delivery preference no longer overrides the checkbox the order just rendered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
f81c1ca8e7
commit
e625b0c2f3
11 changed files with 873 additions and 746 deletions
|
|
@ -1,6 +1,7 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
|
||||
import json
|
||||
from datetime import timedelta
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
|
@ -471,22 +472,25 @@ class TestOnlinePaymentRoutes(HttpCase):
|
|||
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 _publish_a_provider(self):
|
||||
"""Make one provider usable, so the checkout renders the payment form."""
|
||||
provider = self.env.ref("payment.payment_provider_transfer")
|
||||
provider.write(
|
||||
{
|
||||
"state": "test",
|
||||
"is_published": True,
|
||||
"company_id": self.group_order.company_id.id,
|
||||
}
|
||||
)
|
||||
return provider
|
||||
|
||||
def test_payment_page_needs_a_draft(self):
|
||||
"""With nothing in the cart there is nothing to pay for."""
|
||||
def test_legacy_payment_step_redirects_to_checkout(self):
|
||||
"""The separate payment step is gone; its URL lands on the checkout.
|
||||
|
||||
Kept as a redirect rather than dropped so bookmarks, and sessions
|
||||
that were mid-flow when the step disappeared, do not hit a 404.
|
||||
"""
|
||||
self._create_draft()
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
|
||||
response = self.url_open(self._slug_url("/payment"), allow_redirects=False)
|
||||
|
|
@ -494,8 +498,8 @@ class TestOnlinePaymentRoutes(HttpCase):
|
|||
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."""
|
||||
def test_legacy_payment_step_redirects_without_online_payment(self):
|
||||
"""Same for a group order that takes no payments at all."""
|
||||
self.group_order.online_payment = False
|
||||
self._create_draft()
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
|
|
@ -505,22 +509,69 @@ class TestOnlinePaymentRoutes(HttpCase):
|
|||
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.
|
||||
"""
|
||||
def test_checkout_renders_the_payment_form(self):
|
||||
"""Picking a payment method happens on the checkout, in one screen."""
|
||||
self._publish_a_provider()
|
||||
self._create_draft()
|
||||
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)
|
||||
self.assertIn(
|
||||
'id="payment_method"',
|
||||
response.text,
|
||||
"The payment form belongs on the checkout page",
|
||||
)
|
||||
self.assertIn(
|
||||
'data-name="Eskaera Checkout"',
|
||||
response.text,
|
||||
"There must be no separate payment step to redirect to",
|
||||
)
|
||||
|
||||
def test_checkout_summary_comes_from_the_order(self):
|
||||
"""The summary is the order, so it cannot drift from what is charged.
|
||||
|
||||
The cart lives in localStorage while the member shops, and the old
|
||||
client-rendered summary was a snapshot of it taken at page load: a
|
||||
draft reloaded afterwards silently added its own lines back, which
|
||||
only became visible one page later, at payment time.
|
||||
"""
|
||||
self._publish_a_provider()
|
||||
self._create_draft()
|
||||
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(self.product.name, response.text)
|
||||
self.assertNotIn(
|
||||
'id="checkout-summary-tbody"',
|
||||
response.text,
|
||||
"The summary must not be the client-rendered table any more",
|
||||
)
|
||||
|
||||
def test_checkout_without_a_draft_shows_the_empty_state(self):
|
||||
"""Nothing saved for the cycle: nothing to summarise and nothing to pay."""
|
||||
self._publish_a_provider()
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
|
||||
response = self.url_open(self._slug_url("/checkout"), allow_redirects=True)
|
||||
|
||||
# Asserted on markup rather than the empty-state wording: the website
|
||||
# runs in whatever language the visitor picked.
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotIn(
|
||||
"checkout-summary-table",
|
||||
response.text,
|
||||
"With no order there is nothing to summarise",
|
||||
)
|
||||
self.assertNotIn('id="payment_method"', 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._create_draft()
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
|
||||
response = self.url_open(self._slug_url("/checkout"), allow_redirects=True)
|
||||
|
|
@ -566,3 +617,120 @@ class TestOnlinePaymentRoutes(HttpCase):
|
|||
|
||||
self.assertEqual(response.status_code, 303)
|
||||
self.assertIn(f"/payment/confirmation/{order.id}", response.headers["Location"])
|
||||
|
||||
def _post_json(self, route, payload):
|
||||
return self.url_open(
|
||||
route,
|
||||
data=json.dumps(payload),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
def _save_cart(self, quantity, is_delivery=None):
|
||||
payload = {
|
||||
"order_id": self.group_order.id,
|
||||
"items": [
|
||||
{
|
||||
"product_id": self.product.id,
|
||||
"product_name": self.product.name,
|
||||
"quantity": quantity,
|
||||
"product_price": 10.0,
|
||||
}
|
||||
],
|
||||
}
|
||||
if is_delivery is not None:
|
||||
payload["is_delivery"] = is_delivery
|
||||
return self._post_json("/eskaera/save-order", payload)
|
||||
|
||||
def _cycle_drafts(self):
|
||||
return self.env["sale.order"].search(
|
||||
[
|
||||
("partner_id", "=", self.member_partner.id),
|
||||
("group_order_id", "=", self.group_order.id),
|
||||
("state", "=", "draft"),
|
||||
]
|
||||
)
|
||||
|
||||
def test_saving_the_cart_again_updates_the_same_draft(self):
|
||||
"""Pushing the cart reuses the cycle draft rather than adding another.
|
||||
|
||||
The cart reaches the server from the shop's save button and again on
|
||||
the way into the checkout, so this runs on every ordinary flow.
|
||||
"""
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
|
||||
first = self._save_cart(1)
|
||||
self.assertEqual(first.status_code, 200)
|
||||
order_id = first.json()["sale_order_id"]
|
||||
|
||||
second = self._save_cart(3)
|
||||
self.assertEqual(second.status_code, 200)
|
||||
self.assertEqual(
|
||||
second.json()["sale_order_id"],
|
||||
order_id,
|
||||
"A second save must update the draft, never create a new one",
|
||||
)
|
||||
|
||||
drafts = self._cycle_drafts()
|
||||
self.assertEqual(len(drafts), 1)
|
||||
self.assertEqual(drafts.order_line.product_uom_qty, 3)
|
||||
|
||||
def test_saving_an_unchanged_cart_leaves_the_lines_alone(self):
|
||||
"""Nothing changed, nothing rewritten.
|
||||
|
||||
Replacing `order_line` unlinks and recreates every line, so an
|
||||
idempotent save would churn their ids for no reason.
|
||||
"""
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
|
||||
self._save_cart(2)
|
||||
line_ids = self._cycle_drafts().order_line.ids
|
||||
|
||||
self._save_cart(2)
|
||||
|
||||
self.assertEqual(
|
||||
self._cycle_drafts().order_line.ids,
|
||||
line_ids,
|
||||
"An unchanged cart must not rewrite the order lines",
|
||||
)
|
||||
|
||||
def test_home_delivery_toggle_moves_the_line_on_the_order(self):
|
||||
"""The checkout toggle writes to the order, which is what gets charged."""
|
||||
delivery_product = self.env["product.product"].create(
|
||||
{"name": "Home Delivery", "type": "service", "list_price": 5.0}
|
||||
)
|
||||
self.group_order.delivery_product_id = delivery_product
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
self._save_cart(1)
|
||||
|
||||
response = self._post_json(
|
||||
"/eskaera/set-home-delivery",
|
||||
{"order_id": self.group_order.id, "is_delivery": True},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(response.json()["is_delivery"])
|
||||
draft = self._cycle_drafts()
|
||||
self.assertTrue(draft.home_delivery)
|
||||
self.assertIn(delivery_product, draft.order_line.product_id)
|
||||
|
||||
response = self._post_json(
|
||||
"/eskaera/set-home-delivery",
|
||||
{"order_id": self.group_order.id, "is_delivery": False},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFalse(response.json()["is_delivery"])
|
||||
draft = self._cycle_drafts()
|
||||
self.assertFalse(draft.home_delivery)
|
||||
self.assertNotIn(delivery_product, draft.order_line.product_id)
|
||||
|
||||
def test_home_delivery_toggle_needs_a_draft(self):
|
||||
"""With nothing saved there is no order to put the delivery line on."""
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
|
||||
response = self._post_json(
|
||||
"/eskaera/set-home-delivery",
|
||||
{"order_id": self.group_order.id, "is_delivery": True},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue