feat: implement coupon code manage

ment with billing and transaction integration; bug fixes
This commit is contained in:
Joedeep Singh
2025-10-13 13:03:43 +00:00
parent bf36890bd3
commit 6933105261
10 changed files with 221 additions and 154 deletions

View File

@@ -24,15 +24,28 @@
:label="__('Discount Amount')"
type="number"
/>
<FormControl v-model="doc.expires_on" :label="__('Expires On')" type="date" />
<FormControl v-model="doc.usage_limit" :label="__('Usage Limit')" type="number" />
<FormControl
v-model="doc.expires_on"
:label="__('Expires On')"
type="date"
:description="__('Leave blank for no expiry')"
/>
<FormControl
v-model="doc.usage_limit"
:label="__('Usage Limit')"
type="number"
:placeholder="__('Unlimited')"
/>
<Switch v-model="doc.active" :label="__('Active')" />
<div class="col-span-2">
<div class="text-sm font-medium text-ink-gray-7 mb-2">{{ __('Applicable Items (optional)') }}</div>
<div class="text-md font-medium text-ink-gray-7 mb-1 mt-2">{{ __('Select Courses/Batches') }}<span class="text-ink-red-3">*</span></div>
<div class="space-y-2">
<div v-for="(row, idx) in doc.applicable_items" :key="idx" class="flex gap-2 items-center">
<FormControl v-model="row.reference_doctype" :label="__('Type')" type="select" :options="['LMS Course', 'LMS Batch']" />
<Link :doctype="row.reference_doctype || 'LMS Course'" :label="__('Item')" :value="row.reference_name" @change="(opt) => (row.reference_name = opt)" />
<div v-for="(row, idx) in doc.applicable_items" :key="idx" class="flex gap-2 items-end">
<FormControl class="w-28" v-model="row.reference_doctype" :label="__('Type')" type="select" :options="[
{ label: 'Course ', value: 'LMS Course' },
{ label: 'Batch ', value: 'LMS Batch' }
]" />
<Link class="min-w-40" :doctype="row.reference_doctype || 'LMS Course'" :label="__('Name')" :value="row.reference_name" @change="(opt) => (row.reference_name = opt)" />
<Button variant="subtle" @click="removeRow(idx)">
<X class="h-3 w-3" />
</Button>
@@ -135,7 +148,6 @@ function save() {
emit('saved')
},
onError(err) {
console.log('Save error:', err)
toast.error(err.messages?.[0] || err.message || err)
}
})
@@ -147,7 +159,6 @@ function save() {
emit('saved')
},
onError(err) {
console.log('Insert error:', err)
toast.error(err.messages?.[0] || err.message || err)
}
})

View File

@@ -27,6 +27,7 @@
<th class="text-left p-2">{{ __('Expires On') }}</th>
<th class="text-left p-2">{{ __('Usage') }}</th>
<th class="text-left p-2">{{ __('Active') }}</th>
<th class="text-right p-2 w-8"></th>
</tr>
</thead>
<tbody>
@@ -43,6 +44,13 @@
<Badge v-if="row.active" theme="green">{{ __('Enabled') }}</Badge>
<Badge v-else theme="gray">{{ __('Disabled') }}</Badge>
</td>
<td class="p-2 text-right" @click.stop>
<Button variant="ghost" @click="confirmDelete(row)">
<template #icon>
<Trash2 class="h-4 w-4 stroke-1.5 text-ink-red-4" />
</template>
</Button>
</td>
</tr>
</tbody>
</table>
@@ -52,11 +60,14 @@
</div>
</template>
<script setup>
import { Button, Badge, createListResource } from 'frappe-ui'
import { ref } from 'vue'
import { Plus } from 'lucide-vue-next'
import { Button, Badge, createListResource, toast, call } from 'frappe-ui'
import { ref, getCurrentInstance } from 'vue'
import { Plus, Trash2 } from 'lucide-vue-next'
import CouponDetails from '@/components/Settings/CouponDetails.vue'
const app = getCurrentInstance()
const { $dialog } = app.appContext.config.globalProperties
defineProps({
label: String,
description: String,
@@ -78,6 +89,7 @@ const coupons = createListResource({
'times_redeemed',
'active',
],
auto: true,
})
function openForm(id) {
@@ -88,5 +100,31 @@ function openForm(id) {
function onSaved() {
coupons.reload()
}
function confirmDelete(row) {
$dialog({
title: __('Delete this coupon?'),
message: __('This will permanently delete the coupon and the code will no longer work. Are you sure?'),
actions: [
{
label: __('Delete'),
theme: 'red',
variant: 'solid',
onClick({ close }) {
trashCoupon(row.name, close)
close()
},
},
],
})
}
function trashCoupon(name, close) {
call('frappe.client.delete', { doctype: 'LMS Coupon', name }).then(() => {
toast.success(__('Coupon deleted successfully'))
coupons.reload()
if (typeof close === 'function') close()
})
}
</script>

View File

@@ -67,43 +67,32 @@
/>
<FormControl :label="__('Amount')" v-model="transactionData.amount" />
<FormControl
:label="__('Order ID')"
v-model="transactionData.order_id"
v-if="transactionData.coupon"
:label="__('Coupon Code')"
v-model="transactionData.coupon"
:disabled="true"
/>
</div>
<div
v-if="transactionData && (transactionData.coupon || transactionData.discount_amount || transactionData.discount_percent)"
class="mt-10"
>
<div class="font-semibold">
{{ __('Coupon (if applied)') }}
</div>
<div class="grid grid-cols-3 gap-5 mt-5">
<Link
:label="__('Coupon')"
v-model="transactionData.coupon"
doctype="LMS Coupon"
:disabled="true"
/>
<FormControl
:label="__('Discount Type')"
v-model="transactionData.discount_type"
:disabled="true"
/>
<FormControl
v-if="transactionData.discount_type === 'Percent'"
:label="__('Discount Percent')"
v-model="transactionData.discount_percent"
:disabled="true"
/>
<FormControl
v-else
:label="__('Discount Amount')"
v-model="transactionData.discount_amount"
:disabled="true"
/>
</div>
<div class="grid grid-cols-3 gap-5 mt-5">
<FormControl
v-if="Number(transactionData.discount_amount)"
:label="__('Discount Amount')"
v-model="transactionData.discount_amount"
:disabled="true"
/>
<FormControl
v-if="Number(transactionData.gst_amount)"
:label="__('GST Amount')"
v-model="transactionData.gst_amount"
:disabled="true"
/>
<FormControl
v-if="Number(transactionData.discount_amount) || Number(transactionData.gst_amount)"
:label="__('Total Amount')"
v-model="transactionData.total_amount"
:disabled="true"
/>
</div>
<div class="grid grid-cols-3 gap-5 mt-5">
@@ -114,6 +103,13 @@
v-model="transactionData.payment_id"
/>
</div>
<div class="grid grid-cols-3 gap-5 mt-5">
<FormControl
:label="__('Order ID')"
v-model="transactionData.order_id"
/>
</div>
</div>
</template>
<template #actions="{ close }">

View File

@@ -73,7 +73,7 @@
:disabled="true"
/>
<div v-else-if="column.key == 'amount'">
{{ getCurrencySymbol(row['currency']) }} {{ row[column.key] }}
{{ getCurrencySymbol(row['currency']) }} {{ row['total_amount'] }}
</div>
<div v-else class="leading-5 text-sm">
{{ row[column.key] }}
@@ -153,6 +153,7 @@ const transactions = createListResource({
'payment_for_certificate',
'currency',
'amount',
'total_amount',
'order_id',
'payment_id',
'gstin',

View File

@@ -35,6 +35,10 @@
{{ 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"
@@ -46,10 +50,6 @@
{{ orderSummary.data.gst_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
class="flex items-center justify-between border-t border-outline-gray-3 pt-4 mt-2"
>
@@ -63,9 +63,9 @@
<div class="mb-5">
<div class="flex items-center gap-3 mt-2 flex-wrap md:flex-nowrap" v-if="props.type !== 'certificate'">
<span class="text-ink-gray-5 text-xs shrink-0">{{ __('Coupon') }}</span>
<FormControl class="flex-1 min-w-0 [&_input]:!bg-[#fefefe]" v-model="couponCode" @input="couponCode = $event.target.value.toUpperCase()"/>
<Button @click="applyCouponCode" variant="outline">{{ __('Apply') }}</Button>
<Button v-if="appliedCoupon" @click="removeCoupon" variant="subtle">{{ __('Remove') }}</Button>
<FormControl class="flex-1 min-w-0 [&_input]:!bg-[#fefefe]" v-model="couponCode" :disabled="appliedCoupon" @input="couponCode = $event.target.value.toUpperCase()"/>
<Button v-if="!appliedCoupon" @click="applyCouponCode" variant="outline">{{ __('Apply') }}</Button>
<Button v-if="appliedCoupon" @click="removeCoupon" variant="subtle" class="bg-red-200"><X class="h-4.5 w-4.5" /></Button>
</div>
</div>
</div>
@@ -174,6 +174,7 @@ import { reactive, inject, onMounted, computed, ref } from 'vue'
import { sessionStore } from '../stores/session'
import Link from '@/components/Controls/Link.vue'
import NotPermitted from '@/components/NotPermitted.vue'
import { X } from 'lucide-vue-next'
const user = inject('$user')
const { brand } = sessionStore()
@@ -239,6 +240,7 @@ const applyCoupon = createResource({
},
onSuccess(data) {
orderSummary.data = data
console.log('orderSummary.data - ', orderSummary.data)
appliedCoupon.value = couponCode.value
toast.success(__('Coupon applied'))
},
@@ -266,18 +268,20 @@ const setBillingDetails = (data) => {
const paymentLink = createResource({
url: 'lms.lms.payments.get_payment_link',
makeParams(values) {
return {
let data={
doctype: props.type == 'batch' ? 'LMS Batch' : 'LMS Course',
docname: props.name,
title: orderSummary.data.title,
amount: orderSummary.data.original_amount,
total_amount: orderSummary.data.amount,
discount_amount: orderSummary.data.discount_amount || 0,
gst_amount: orderSummary.data.gst_applied || 0,
currency: orderSummary.data.currency,
address: billingDetails,
redirect_to: redirectTo.value,
payment_for_certificate: props.type == 'certificate',
coupon_code: appliedCoupon.value,
}
return data
},
})
@@ -308,6 +312,7 @@ function applyCouponCode() {
function removeCoupon() {
appliedCoupon.value = null
couponCode.value = ''
orderSummary.reload()
}