fix: coupon code application on billing page
This commit is contained in:
@@ -13,83 +13,81 @@
|
|||||||
class="pt-5 pb-10 mx-5"
|
class="pt-5 pb-10 mx-5"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col lg:flex-row justify-between">
|
<div class="flex flex-col lg:flex-row justify-between">
|
||||||
<div
|
<div class="flex flex-col lg:order-last mb-10 lg:mt-10 lg:w-1/4">
|
||||||
class="h-fit bg-surface-gray-2 rounded-md p-5 space-y-4 lg:order-last mb-10 lg:mt-10 font-medium lg:w-1/3"
|
<div class="h-fit bg-surface-gray-2 rounded-md p-5 space-y-4">
|
||||||
>
|
<div class="space-y-1">
|
||||||
<div class="flex items-baseline justify-between space-y-2">
|
<div class="text-ink-gray-5 uppercase text-xs">
|
||||||
<div class="text-ink-gray-5">
|
{{ __('Payment for ') }} {{ type }}:
|
||||||
{{ __('Payment for ') }} {{ type }}:
|
</div>
|
||||||
|
<div class="leading-5 text-ink-gray-9">
|
||||||
|
{{ orderSummary.data.title }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="leading-5 text-ink-gray-9">
|
|
||||||
{{ orderSummary.data.title }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="orderSummary.data.gst_applied"
|
|
||||||
class="flex items-center justify-between"
|
|
||||||
>
|
|
||||||
<div class="text-ink-gray-5">
|
|
||||||
{{ __('Original Amount') }}
|
|
||||||
</div>
|
|
||||||
<div class="text-ink-gray-9">
|
|
||||||
{{ orderSummary.data.original_amount_formatted }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="orderSummary.data.discount_amount"
|
|
||||||
class="flex items-center justify-between mt-2"
|
|
||||||
>
|
|
||||||
<div class="text-ink-gray-5">{{ __('Discount') }}</div>
|
|
||||||
<div>-{{ orderSummary.data.discount_amount_formatted }}</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="orderSummary.data.gst_applied"
|
|
||||||
class="flex items-center justify-between mt-2"
|
|
||||||
>
|
|
||||||
<div class="text-ink-gray-5">
|
|
||||||
{{ __('GST Amount') }}
|
|
||||||
</div>
|
|
||||||
<div class="text-ink-gray-9">
|
|
||||||
{{ orderSummary.data.gst_amount_formatted }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-between border-t border-outline-gray-3 pt-4 mt-2"
|
|
||||||
>
|
|
||||||
<div class="text-lg font-semibold text-ink-gray-9">
|
|
||||||
{{ __('Total') }}
|
|
||||||
</div>
|
|
||||||
<div class="text-lg font-semibold text-ink-gray-9">
|
|
||||||
{{ orderSummary.data.total_amount_formatted }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-5">
|
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-3 mt-2 flex-wrap md:flex-nowrap"
|
v-if="
|
||||||
v-if="props.type !== 'certificate'"
|
orderSummary.data.gst_applied ||
|
||||||
|
orderSummary.data.discount_amount
|
||||||
|
"
|
||||||
|
class="space-y-1"
|
||||||
>
|
>
|
||||||
<span class="text-ink-gray-5 text-xs shrink-0">{{
|
<div class="text-ink-gray-5 uppercase text-xs">
|
||||||
__('Coupon')
|
{{ __('Original Amount') }}:
|
||||||
}}</span>
|
</div>
|
||||||
|
<div class="text-ink-gray-9">
|
||||||
|
{{ orderSummary.data.original_amount_formatted }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="orderSummary.data.discount_amount" class="space-y-1">
|
||||||
|
<div class="text-ink-gray-5">{{ __('Discount') }}:</div>
|
||||||
|
<div>- {{ orderSummary.data.discount_amount_formatted }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="orderSummary.data.gst_applied" class="space-y-1">
|
||||||
|
<div class="text-ink-gray-5 uppercase text-xs">
|
||||||
|
{{ __('GST Amount') }}:
|
||||||
|
</div>
|
||||||
|
<div class="text-ink-gray-9">
|
||||||
|
{{ orderSummary.data.gst_amount_formatted }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1 border-t border-outline-gray-3 pt-4 mt-2">
|
||||||
|
<div class="uppercase text-ink-gray-5 text-xs">
|
||||||
|
{{ __('Total') }}:
|
||||||
|
</div>
|
||||||
|
<div class="font-bold text-ink-gray-9">
|
||||||
|
{{ orderSummary.data.total_amount_formatted }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-surface-gray-2 rounded-md p-4 space-y-2 my-5">
|
||||||
|
<span class="text-ink-gray-5 uppercase text-xs">
|
||||||
|
{{ __('Enter a Coupon Code') }}:
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
<FormControl
|
<FormControl
|
||||||
class="flex-1 min-w-0 [&_input]:!bg-[#fefefe]"
|
v-model="appliedCoupon"
|
||||||
v-model="couponCode"
|
:disabled="orderSummary.data.discount_amount > 0"
|
||||||
:disabled="appliedCoupon"
|
@input="appliedCoupon = $event.target.value.toUpperCase()"
|
||||||
@input="couponCode = $event.target.value.toUpperCase()"
|
@keydown.enter="applyCouponCode"
|
||||||
|
placeholder="COUPON2025"
|
||||||
|
class="flex-1 [&_input]:bg-white"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
v-if="!appliedCoupon"
|
v-if="!orderSummary.data.discount_amount"
|
||||||
@click="applyCouponCode"
|
@click="applyCouponCode"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>{{ __('Apply') }}</Button
|
|
||||||
>
|
>
|
||||||
|
{{ __('Apply') }}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
v-if="appliedCoupon"
|
v-if="orderSummary.data.discount_amount"
|
||||||
@click="removeCoupon"
|
@click="removeCoupon"
|
||||||
variant="subtle"
|
variant="outline"
|
||||||
class="bg-red-200"
|
>
|
||||||
><X class="h-4.5 w-4.5"
|
<template #icon>
|
||||||
/></Button>
|
<X class="size-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -148,7 +146,7 @@
|
|||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-if="billingDetails.country == 'India'"
|
v-if="billingDetails.country == 'India'"
|
||||||
:label="__('Pan Number')"
|
:label="__('PAN Number')"
|
||||||
v-model="billingDetails.pan"
|
v-model="billingDetails.pan"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -193,6 +191,7 @@ import {
|
|||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
usePageMeta,
|
usePageMeta,
|
||||||
toast,
|
toast,
|
||||||
|
call,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { reactive, inject, onMounted, computed, ref } from 'vue'
|
import { reactive, inject, onMounted, computed, ref } from 'vue'
|
||||||
import { sessionStore } from '../stores/session'
|
import { sessionStore } from '../stores/session'
|
||||||
@@ -238,10 +237,12 @@ const access = createResource({
|
|||||||
const orderSummary = createResource({
|
const orderSummary = createResource({
|
||||||
url: 'lms.lms.utils.get_order_summary',
|
url: 'lms.lms.utils.get_order_summary',
|
||||||
makeParams(values) {
|
makeParams(values) {
|
||||||
|
console.log(appliedCoupon.value)
|
||||||
return {
|
return {
|
||||||
doctype: props.type == 'batch' ? 'LMS Batch' : 'LMS Course',
|
doctype: props.type == 'batch' ? 'LMS Batch' : 'LMS Course',
|
||||||
docname: props.name,
|
docname: props.name,
|
||||||
country: billingDetails.country,
|
country: billingDetails.country,
|
||||||
|
coupon: appliedCoupon.value,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError(err) {
|
onError(err) {
|
||||||
@@ -249,29 +250,7 @@ const orderSummary = createResource({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const couponCode = ref('')
|
|
||||||
const appliedCoupon = ref(null)
|
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 billingDetails = reactive({})
|
||||||
|
|
||||||
const setBillingDetails = (data) => {
|
const setBillingDetails = (data) => {
|
||||||
@@ -329,13 +308,15 @@ const generatePaymentLink = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyCouponCode() {
|
function applyCouponCode() {
|
||||||
if (!couponCode.value) return
|
if (!appliedCoupon.value) {
|
||||||
applyCoupon.submit()
|
toast.error(__('Please enter a coupon code'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
orderSummary.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeCoupon() {
|
function removeCoupon() {
|
||||||
appliedCoupon.value = null
|
appliedCoupon.value = null
|
||||||
couponCode.value = ''
|
|
||||||
orderSummary.reload()
|
orderSummary.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
143
lms/lms/utils.py
143
lms/lms/utils.py
@@ -953,73 +953,6 @@ def get_current_exchange_rate(source, target="USD"):
|
|||||||
return details["rates"][target]
|
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()
|
@frappe.whitelist()
|
||||||
def change_currency(amount, currency, country=None):
|
def change_currency(amount, currency, country=None):
|
||||||
amount = cint(amount)
|
amount = cint(amount)
|
||||||
@@ -1819,7 +1752,7 @@ def get_discussion_replies(topic):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_order_summary(doctype, docname, country=None):
|
def get_order_summary(doctype, docname, coupon=None, country=None):
|
||||||
if doctype == "LMS Course":
|
if doctype == "LMS Course":
|
||||||
details = frappe.db.get_value(
|
details = frappe.db.get_value(
|
||||||
"LMS Course",
|
"LMS Course",
|
||||||
@@ -1856,6 +1789,12 @@ def get_order_summary(doctype, docname, country=None):
|
|||||||
details.original_amount = details.amount
|
details.original_amount = details.amount
|
||||||
details.original_amount_formatted = fmt_money(details.amount, 0, details.currency)
|
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":
|
if details.currency == "INR":
|
||||||
details.amount, details.gst_applied = apply_gst(details.amount, country)
|
details.amount, details.gst_applied = apply_gst(details.amount, country)
|
||||||
details.gst_amount_formatted = fmt_money(details.gst_applied, 0, details.currency)
|
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
|
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()
|
@frappe.whitelist()
|
||||||
def get_lesson_creation_details(course, chapter, lesson):
|
def get_lesson_creation_details(course, chapter, lesson):
|
||||||
chapter_name = frappe.db.get_value("Chapter Reference", {"parent": course, "idx": chapter}, "chapter")
|
chapter_name = frappe.db.get_value("Chapter Reference", {"parent": course, "idx": chapter}, "chapter")
|
||||||
|
|||||||
Reference in New Issue
Block a user