From 18e499e6dee7f0463bbaf3ebf4e6d3fe7f641734 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Tue, 4 Nov 2025 15:12:17 +0530 Subject: [PATCH] fix: coupon code application on billing page --- frontend/src/pages/Billing.vue | 165 +++++++++++++++------------------ lms/lms/utils.py | 143 ++++++++++++++-------------- 2 files changed, 148 insertions(+), 160 deletions(-) diff --git a/frontend/src/pages/Billing.vue b/frontend/src/pages/Billing.vue index 3bac2ca0..dc89a738 100644 --- a/frontend/src/pages/Billing.vue +++ b/frontend/src/pages/Billing.vue @@ -13,83 +13,81 @@ class="pt-5 pb-10 mx-5" >
-
-
-
- {{ __('Payment for ') }} {{ type }}: +
+
+
+
+ {{ __('Payment for ') }} {{ type }}: +
+
+ {{ orderSummary.data.title }} +
-
- {{ orderSummary.data.title }} -
-
-
-
- {{ __('Original Amount') }} -
-
- {{ orderSummary.data.original_amount_formatted }} -
-
-
-
{{ __('Discount') }}
-
-{{ orderSummary.data.discount_amount_formatted }}
-
-
-
- {{ __('GST Amount') }} -
-
- {{ orderSummary.data.gst_amount_formatted }} -
-
-
-
- {{ __('Total') }} -
-
- {{ orderSummary.data.total_amount_formatted }} -
-
-
- {{ - __('Coupon') - }} +
+ {{ __('Original Amount') }}: +
+
+ {{ orderSummary.data.original_amount_formatted }} +
+
+
+
{{ __('Discount') }}:
+
- {{ orderSummary.data.discount_amount_formatted }}
+
+
+
+ {{ __('GST Amount') }}: +
+
+ {{ orderSummary.data.gst_amount_formatted }} +
+
+
+
+ {{ __('Total') }}: +
+
+ {{ orderSummary.data.total_amount_formatted }} +
+
+
+ +
+ + {{ __('Enter a Coupon Code') }}: + +
+ {{ __('Apply') }} + + variant="outline" + > + +
@@ -148,7 +146,7 @@ />
@@ -193,6 +191,7 @@ import { Breadcrumbs, usePageMeta, toast, + call, } from 'frappe-ui' import { reactive, inject, onMounted, computed, ref } from 'vue' import { sessionStore } from '../stores/session' @@ -238,10 +237,12 @@ const access = createResource({ const orderSummary = createResource({ url: 'lms.lms.utils.get_order_summary', makeParams(values) { + console.log(appliedCoupon.value) return { doctype: props.type == 'batch' ? 'LMS Batch' : 'LMS Course', docname: props.name, country: billingDetails.country, + coupon: appliedCoupon.value, } }, onError(err) { @@ -249,29 +250,7 @@ const orderSummary = createResource({ }, }) -const couponCode = ref('') const appliedCoupon = ref(null) - -const applyCoupon = createResource({ - url: 'lms.lms.utils.apply_coupon', - makeParams() { - return { - doctype: props.type == 'batch' ? 'LMS Batch' : 'LMS Course', - docname: props.name, - code: couponCode.value, - country: billingDetails.country, - } - }, - onSuccess(data) { - orderSummary.data = data - appliedCoupon.value = couponCode.value - toast.success(__('Coupon applied')) - }, - onError(err) { - toast.error(err.messages?.[0] || err) - }, -}) - const billingDetails = reactive({}) const setBillingDetails = (data) => { @@ -329,13 +308,15 @@ const generatePaymentLink = () => { } function applyCouponCode() { - if (!couponCode.value) return - applyCoupon.submit() + if (!appliedCoupon.value) { + toast.error(__('Please enter a coupon code')) + return + } + orderSummary.reload() } function removeCoupon() { appliedCoupon.value = null - couponCode.value = '' orderSummary.reload() } diff --git a/lms/lms/utils.py b/lms/lms/utils.py index 927079fc..070c0183 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -953,73 +953,6 @@ def get_current_exchange_rate(source, target="USD"): return details["rates"][target] -@frappe.whitelist() -def apply_coupon(doctype, docname, code, country=None): - if doctype not in ["LMS Course", "LMS Batch"]: - frappe.throw(_("Invalid doctype for coupon application.")) - - if not code: - frappe.throw(_("Coupon code is required.")) - - summary = get_order_summary(doctype, docname, country) - - base_amount = summary.original_amount - currency = summary.currency - - coupon_name = frappe.db.get_value("LMS Coupon", {"code": code.strip().upper(), "active": 1}, "name") - if not coupon_name: - frappe.throw(_("Invalid or inactive coupon code.")) - - coupon = frappe.get_doc("LMS Coupon", coupon_name) - - if coupon.expires_on and getdate(coupon.expires_on) < getdate(): - frappe.throw(_("This coupon has expired.")) - - if coupon.usage_limit and cint(coupon.times_redeemed) >= cint(coupon.usage_limit): - frappe.throw(_("This coupon has reached its usage limit.")) - - # check applicability - applicable = True - if len(coupon.applicable_items): - applicable = any( - (row.reference_doctype == doctype and row.reference_name == docname) - for row in coupon.applicable_items - ) - if not applicable: - frappe.throw(_("This coupon is not applicable to this item.")) - - discount_amount = 0 - if coupon.discount_type == "Percent": - discount_amount = cint(flt(base_amount) * flt(coupon.percent_off) / 100) - else: - discount_amount = min(flt(coupon.amount_off), flt(base_amount)) - - subtotal = max(flt(base_amount) - flt(discount_amount), 0) - - gst_applied = 0 - final_amount = subtotal - if currency == "INR": - final_amount, gst_applied = apply_gst(subtotal, country) - - return { - "title": summary.title, - "name": summary.name, - "currency": currency, - "original_amount": base_amount, - "original_amount_formatted": fmt_money(base_amount, 0, currency), - "discount_amount": discount_amount, - "discount_amount_formatted": fmt_money(discount_amount, 0, currency), - "amount": final_amount, - "gst_applied": gst_applied, - "gst_amount_formatted": fmt_money(gst_applied, 0, currency) if gst_applied else None, - "total_amount_formatted": fmt_money(final_amount, 0, currency), - "coupon": coupon.name, - "coupon_code": coupon.code, - "discount_type": coupon.discount_type, - "discount_percent": coupon.percent_off if coupon.discount_type == "Percent" else None, - } - - @frappe.whitelist() def change_currency(amount, currency, country=None): amount = cint(amount) @@ -1819,7 +1752,7 @@ def get_discussion_replies(topic): @frappe.whitelist() -def get_order_summary(doctype, docname, country=None): +def get_order_summary(doctype, docname, coupon=None, country=None): if doctype == "LMS Course": details = frappe.db.get_value( "LMS Course", @@ -1856,6 +1789,12 @@ def get_order_summary(doctype, docname, country=None): details.original_amount = details.amount details.original_amount_formatted = fmt_money(details.amount, 0, details.currency) + if coupon: + discount_amount, subtotal = apply_coupon(doctype, docname, coupon, details.amount) + details.amount = subtotal + details.discount_amount = discount_amount + details.discount_amount_formatted = fmt_money(discount_amount, 0, details.currency) + if details.currency == "INR": details.amount, details.gst_applied = apply_gst(details.amount, country) details.gst_amount_formatted = fmt_money(details.gst_applied, 0, details.currency) @@ -1864,6 +1803,74 @@ def get_order_summary(doctype, docname, country=None): return details +def apply_coupon(doctype, docname, code, base_amount): + coupon_name = frappe.db.exists("LMS Coupon", {"code": code, "enabled": 1}) + + if not coupon_name: + frappe.throw(_("Invalid or inactive coupon code.")) + + coupon = frappe.db.get_value( + "LMS Coupon", + coupon_name, + [ + "expires_on", + "usage_limit", + "redemption_count", + "discount_type", + "percentage_discount", + "fixed_amount_discount", + "name", + "code", + ], + as_dict=True, + ) + + validate_coupon(doctype, code, coupon) + validate_coupon_applicability(doctype, docname, coupon_name) + + discount_amount = calculate_discount_amount(base_amount, coupon) + subtotal = max(flt(base_amount) - flt(discount_amount), 0) + + return discount_amount, subtotal + + +def validate_coupon(doctype, code, coupon): + if doctype not in ["LMS Course", "LMS Batch"]: + frappe.throw(_("Invalid doctype for coupon application.")) + + if not code: + frappe.throw(_("Coupon code is required.")) + + if coupon.expires_on and getdate(coupon.expires_on) < getdate(): + frappe.throw(_("This coupon has expired.")) + + if coupon.usage_limit and cint(coupon.redemption_count) >= cint(coupon.usage_limit): + frappe.throw(_("This coupon has reached its maximum usage limit.")) + + +def validate_coupon_applicability(doctype, docname, coupon_name): + applicable_item = frappe.db.exists( + "LMS Coupon Item", {"parent": coupon_name, "reference_doctype": doctype, "reference_name": docname} + ) + if not applicable_item: + frappe.throw( + _("This coupon is not applicable to this {0}.").format( + "Course" if doctype == "LMS Course" else "Batch" + ) + ) + + +def calculate_discount_amount(base_amount, coupon): + discount_amount = 0 + + if coupon.discount_type == "Percentage": + discount_amount = (base_amount * coupon.percentage_discount) / 100 + elif coupon.discount_type == "Fixed Amount": + discount_amount = base_amount - coupon.fixed_amount_discount + + return discount_amount + + @frappe.whitelist() def get_lesson_creation_details(course, chapter, lesson): chapter_name = frappe.db.get_value("Chapter Reference", {"parent": course, "idx": chapter}, "chapter")