Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion commera/api/payments.py
Original file line number Diff line number Diff line change
Expand Up @@ -765,7 +765,10 @@ def apply_coupon_code(applied_code: str):
validate_cart_is_not_in_checkout(quotation.name)
quotation.coupon_code = coupon_name
save_cart_quotation(quotation)
return {"message": _("Coupon code applied successfully")}
return {
"message": _("Coupon code applied successfully"),
"checkout_summary": get_checkout_summary(quotation),
}


# Guest checkout: refuses any caller without a guest cart cookie, and is rate limited.
Expand Down
3 changes: 3 additions & 0 deletions commera/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
website_route_rules = [
# SPA client-side routing: every /commera deep link resolves to the same shell so a reload does not 404.
{"from_route": "/commera/<path:app_path>", "to_route": "/commera"},
{"from_route": "/cart/<path:codes>", "to_route": "/cart/permalink.html"},
# ------------
# English Routes
# ------------
Expand All @@ -40,6 +41,7 @@
# -> Cart / Checkout
{"from_route": "/en/cart", "to_route": "/cart/cart.html"},
{"from_route": "/en/cart/checkout", "to_route": "/cart/checkout.html"},
{"from_route": "/en/cart/<path:codes>", "to_route": "/cart/permalink.html"},
# -> Account
{"from_route": "/en/account/dashboard", "to_route": "/account/dashboard.html"},
{"from_route": "/en/account/profile", "to_route": "/account/profile.html"},
Expand Down Expand Up @@ -67,6 +69,7 @@
# -> Cart / Checkout
{"from_route": "/ar/cart", "to_route": "/cart/cart.html"},
{"from_route": "/ar/cart/checkout", "to_route": "/cart/checkout.html"},
{"from_route": "/ar/cart/<path:codes>", "to_route": "/cart/permalink.html"},
# -> Account
{"from_route": "/ar/account/dashboard", "to_route": "/account/dashboard.html"},
{"from_route": "/ar/account/profile", "to_route": "/account/profile.html"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
"template_path": "pages/cart/checkout.html",
"requires_auth": 0,
},
{
"url_pattern": rf"^{LANG}/cart/(?!checkout$)(?P<codes>.+)$",
"template_path": "pages/cart/permalink.html",
"requires_auth": 0,
},
{
"url_pattern": rf"^{LANG}/account/dashboard$",
"template_path": "pages/account/dashboard.html",
Expand Down
16 changes: 16 additions & 0 deletions commera/templates/includes/cart-permalink.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script>
document.addEventListener("alpine:init", () => {
Alpine.data("cart_permalink", (config) => ({
async init() {
if (!config.lines.length) return;
this.$store.cart.clear();
for (const line of config.lines) {
this.$store.cart.add(line.item, JSON.stringify(line.variant), line.price, line.default_price, line.brand, line.sizes, line.qty);
}
// $persist writes on Alpine's next flush; navigating first can leave the old cart in storage.
await this.$nextTick();
if (!config.notices.length) window.location.replace(config.next_url);
},
}));
});
</script>
19 changes: 19 additions & 0 deletions commera/templates/includes/cart-state.html
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,25 @@
remove_selected() {
this.items = this.items.filter(item => !item.selected);
},
link(discount_code = "") {
const pairs = this.items
.filter(item => item.selected)
.map(item => `${encodeURIComponent(item.variant.item_code)}:${item.qty}`);
const language = window.location.pathname.split("/")[1] === "ar" ? "ar" : "en";
const url = new URL(`/${language}/cart/${pairs.join(",")}`, window.location.origin);
if (discount_code) url.searchParams.set("discount", discount_code);
return url.href;
},
async copy_link(discount_code = "") {
const url = this.link(discount_code);
try {
await navigator.clipboard.writeText(url);
toast({{ _("Cart link copied") | tojson }}, { type: "success" });
} catch (error) {
// The clipboard is refused over plain http and in some in-app browsers.
window.prompt({{ _("Copy this cart link") | tojson }}, url);
}
},
async update_variant(cart_item, selected_size) {
const item_name = cart_item.item.name;
const reponse = await frappe_call("/api/v2/method/commera.api.cart.update_variant", { product_name: item_name, size: selected_size });
Expand Down
6 changes: 5 additions & 1 deletion commera/templates/partials/cart/body.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
}
frappe_call("/api/v2/method/commera.api.payments.generate_quotation_for_cart", { cart: { items: selected_items } })
.then((d) => {
window.location.href = "/{{frappe.lang}}/cart/checkout"
window.location.href = "/{{frappe.lang}}/cart/checkout" + window.location.search
}).finally(() => {
state.checkout_loading = false;
})
Expand Down Expand Up @@ -258,6 +258,10 @@ <h2 class="text-base font-medium md:text-lg capitalize">
</span>
</button>
{% endif %}
<button type="button" x-on:click="$store.cart.copy_link()"
class="mt-3 w-full h-12 py-3 border border-accent rounded-lg text-accent text-base font-semibold uppercase cursor-pointer">
{{ _("Copy cart link") }}
</button>
</div>
</div>
</div>
Expand Down
32 changes: 29 additions & 3 deletions commera/templates/partials/checkout/body.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,19 @@
})
this.apply_shipping_rule()
this.validate_payment_mode()
this.apply_pending_discount()
},
async apply_pending_discount() {
const url = new URL(window.location.href);
const discount = url.searchParams.get("discount");
if (!discount) return;
// Stripped first: removing the coupon reloads this page and must not re-apply it.
url.searchParams.delete("discount");
history.replaceState(null, "", url);
try {
await frappe_call("/api/v2/method/commera.api.payments.apply_coupon_code", { applied_code: discount });
window.location.reload();
} catch (error) {}
},
calculate_checkout_total() {

Expand Down Expand Up @@ -72,6 +85,12 @@


})
function fill_empty_fields(form, prefill) {
for (const [field, value] of Object.entries(prefill)) {
const input = form.elements[`billing_${field}`];
if (input && !input.value) input.value = value;
}
}
function billing_state() {
return {
billing_use_address: 'saved_address',
Expand Down Expand Up @@ -247,8 +266,14 @@
x-init="$watch('payment_mode_confirmed', value => fetch_payment_details(value, $data))">
<div class="flex items-baseline justify-between">
<h1 class="text-3xl font-bold my-3 md:mb-11 md">{{ _('Checkout') }}</h1>
<div class="md:hidden my-3">
{% include 'templates/components/promo_code_component.html' %}
<div class="flex items-center gap-4 my-3">
<div class="md:hidden">
{% include 'templates/components/promo_code_component.html' %}
</div>
<button type="button" @click='$store.cart.copy_link({{ coupon_code | tojson }})'
class="text-md text-secondary-accent underline decoration-accent md:text-base capitalize cursor-pointer">
{{ _("Copy cart link") }}
</button>
</div>
</div>
<div class="flex items-baseline md:hidden">
Expand All @@ -273,7 +298,8 @@ <h2 class="text-lg font-medium me-2 flex">
</div>
<div class="flex flex-col md:flex-row gap-8 md:gap-25 md:mt-2">
<div class="flex-1" x-data="billing_state()">
<form x-ref="checkout_form" @submit.prevent="update_quotation_address($data, $refs)">
<form x-ref="checkout_form" @submit.prevent="update_quotation_address($data, $refs)"
x-init='fill_empty_fields($el, {{ checkout_prefill | tojson }})'>
<div class="flex justify-between items-center border-b border-gray-200 pb-4">
<h2 class="text-lg font-semibold text-secondary-accent capitalize">
{{ _('select delivery option') }}
Expand Down
38 changes: 38 additions & 0 deletions commera/tests/test_cart_checkout.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from commera.api.payments import (
COD_PAYMENT_MODE,
CheckoutPriceChangedError,
apply_coupon_code,
confirm_payment,
generate_quotation_for_cart,
initiate_checkout_with_mode,
Expand Down Expand Up @@ -351,6 +352,43 @@ def test_a_grand_total_coupon_shows_as_a_discount_the_lines_add_up_to(self):
self.assertEqual(summary["total"], round(270 + 48.6 + summary["shipping"] - 50))
self.assert_summary_adds_up(summary)

def create_coupon(self, discount_percentage: float) -> str:
pricing_rule = frappe.get_doc(
{
"doctype": "Pricing Rule",
"title": f"ZZ Coupon {frappe.generate_hash(length=8)}",
"apply_on": "Transaction",
"rate_or_discount": "Discount Percentage",
"discount_percentage": discount_percentage,
"selling": 1,
"coupon_code_based": 1,
"currency": frappe.get_cached_value("Price List", self.sale_price_list, "currency"),
}
).insert(ignore_permissions=True)
coupon_code = f"ZZ{frappe.generate_hash(length=8).upper()}"
frappe.get_doc(
{
"doctype": "Coupon Code",
"coupon_name": coupon_code,
"coupon_code": coupon_code,
"coupon_type": "Promotional",
"pricing_rule": pricing_rule.name,
}
).insert(ignore_permissions=True)
return coupon_code

def test_applying_a_coupon_returns_the_discounted_checkout_summary(self):
coupon_code = self.create_coupon(discount_percentage=10)
frappe.set_user(self.shopper)
generate_quotation_for_cart({"items": [self.cart_line(self.discounted_item, 3)]})
self.assertEqual(get_checkout_summary(_get_cart_quotation())["discount_amount"], 0)

summary = apply_coupon_code(coupon_code)["checkout_summary"]

self.assertEqual(summary["subtotal"], 270)
self.assertAlmostEqual(summary["discount_amount"], (270 + summary["shipping"]) * 0.1, places=2)
self.assert_summary_adds_up(summary)

def test_the_cod_checkout_total_is_what_the_order_charges(self):
ensure_fiscal_year()
frappe.set_user(self.shopper)
Expand Down
Loading
Loading