feat: payment verification and membership
This commit is contained in:
@@ -188,6 +188,10 @@ website_route_rules = [
|
|||||||
"from_route": "/quiz-submission/<quiz>/<submission>",
|
"from_route": "/quiz-submission/<quiz>/<submission>",
|
||||||
"to_route": "quiz_submission/quiz_submission",
|
"to_route": "quiz_submission/quiz_submission",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"from_route": "/billing/<course>",
|
||||||
|
"to_route": "billing/billing",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
website_redirects = [
|
website_redirects = [
|
||||||
|
|||||||
@@ -12,6 +12,12 @@
|
|||||||
"member",
|
"member",
|
||||||
"member_name",
|
"member_name",
|
||||||
"member_username",
|
"member_username",
|
||||||
|
"billing_information_section",
|
||||||
|
"address",
|
||||||
|
"payment_received",
|
||||||
|
"column_break_rvzn",
|
||||||
|
"order_id",
|
||||||
|
"payment_id",
|
||||||
"section_break_8",
|
"section_break_8",
|
||||||
"cohort",
|
"cohort",
|
||||||
"subgroup",
|
"subgroup",
|
||||||
@@ -112,11 +118,42 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "section_break_8",
|
"fieldname": "section_break_8",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "billing_information_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Billing Information"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "address",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Address",
|
||||||
|
"options": "Address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "payment_received",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Payment Received"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_rvzn",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "order_id",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Order ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "payment_id",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Payment ID"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-10-10 12:38:17.839526",
|
"modified": "2023-08-11 15:39:50.194348",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Batch Membership",
|
"name": "LMS Batch Membership",
|
||||||
@@ -141,4 +178,4 @@
|
|||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"title_field": "member_name"
|
"title_field": "member_name"
|
||||||
}
|
}
|
||||||
@@ -359,14 +359,12 @@ def reorder_chapter(chapter_array):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_payment_options(course):
|
def get_payment_options(course, phone):
|
||||||
course_details = frappe.db.get_value(
|
course_details = frappe.db.get_value(
|
||||||
"LMS Course", course, ["name", "title", "currency", "course_price"], as_dict=True
|
"LMS Course", course, ["name", "title", "currency", "course_price"], as_dict=True
|
||||||
)
|
)
|
||||||
razorpay_key = frappe.db.get_single_value("LMS Settings", "razorpay_key")
|
razorpay_key = frappe.db.get_single_value("LMS Settings", "razorpay_key")
|
||||||
razorpay_secret = frappe.db.get_single_value("LMS Settings", "razorpay_secret")
|
client = get_client()
|
||||||
|
|
||||||
client = get_client(razorpay_key, razorpay_secret)
|
|
||||||
order = create_order(client, course_details)
|
order = create_order(client, course_details)
|
||||||
|
|
||||||
options = {
|
options = {
|
||||||
@@ -379,15 +377,32 @@ def get_payment_options(course):
|
|||||||
"prefill": {
|
"prefill": {
|
||||||
"name": frappe.db.get_value("User", frappe.session.user, "full_name"),
|
"name": frappe.db.get_value("User", frappe.session.user, "full_name"),
|
||||||
"email": frappe.session.user,
|
"email": frappe.session.user,
|
||||||
|
"contact": phone,
|
||||||
},
|
},
|
||||||
"callback_url": frappe.utils.get_url(
|
|
||||||
"/api/method/lms.lms.doctype.lms_course.lms_course.verify_payment"
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
return options
|
return options
|
||||||
|
|
||||||
|
|
||||||
def get_client(razorpay_key, razorpay_secret):
|
def save_address(address):
|
||||||
|
address = json.loads(address)
|
||||||
|
address.update(
|
||||||
|
{
|
||||||
|
"address_title": frappe.db.get_value("User", frappe.session.user, "full_name"),
|
||||||
|
"address_type": "Billing",
|
||||||
|
"is_primary_address": 1,
|
||||||
|
"email_id": frappe.session.user,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
doc = frappe.new_doc("Address")
|
||||||
|
doc.update(address)
|
||||||
|
doc.save(ignore_permissions=True)
|
||||||
|
return doc.name
|
||||||
|
|
||||||
|
|
||||||
|
def get_client():
|
||||||
|
razorpay_key = frappe.db.get_single_value("LMS Settings", "razorpay_key")
|
||||||
|
razorpay_secret = frappe.db.get_single_value("LMS Settings", "razorpay_secret")
|
||||||
|
|
||||||
if not razorpay_key and not razorpay_secret:
|
if not razorpay_key and not razorpay_secret:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_(
|
_(
|
||||||
@@ -399,9 +414,48 @@ def get_client(razorpay_key, razorpay_secret):
|
|||||||
|
|
||||||
|
|
||||||
def create_order(client, course_details):
|
def create_order(client, course_details):
|
||||||
return client.order.create(
|
try:
|
||||||
|
return client.order.create(
|
||||||
|
{
|
||||||
|
"amount": course_details.course_price * 100,
|
||||||
|
"currency": course_details.currency,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
frappe.throw(
|
||||||
|
_("Error during payment: {0}. Please contact the Administrator.").format(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def verify_payment(response, course, address, order_id):
|
||||||
|
response = json.loads(response)
|
||||||
|
client = get_client()
|
||||||
|
client.utility.verify_payment_signature(
|
||||||
{
|
{
|
||||||
"amount": course_details.course_price * 100,
|
"razorpay_order_id": order_id,
|
||||||
"currency": course_details.currency,
|
"razorpay_payment_id": response["razorpay_payment_id"],
|
||||||
|
"razorpay_signature": response["razorpay_signature"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return create_membership(address, response, course)
|
||||||
|
|
||||||
|
|
||||||
|
def create_membership(address, response, course):
|
||||||
|
address_name = save_address(address)
|
||||||
|
membership = frappe.new_doc("LMS Batch Membership")
|
||||||
|
|
||||||
|
membership.update(
|
||||||
|
{
|
||||||
|
"member": frappe.session.user,
|
||||||
|
"course": course,
|
||||||
|
"address": address_name,
|
||||||
|
"payment_received": 1,
|
||||||
|
"order_id": response["razorpay_order_id"],
|
||||||
|
"payment_id": response["razorpay_payment_id"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
membership.save(ignore_permissions=True)
|
||||||
|
|
||||||
|
return f"/courses/{course}/learn/1.1"
|
||||||
|
|||||||
@@ -2285,4 +2285,15 @@ select {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-section .section-head {
|
||||||
|
margin-bottom: var(--margin-sm);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--heading-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-column:first-child {
|
||||||
|
padding-right: 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ const expand_the_first_chapter = () => {
|
|||||||
|
|
||||||
const expand_the_active_chapter = () => {
|
const expand_the_active_chapter = () => {
|
||||||
let selector = $(".course-home-headings.title");
|
let selector = $(".course-home-headings.title");
|
||||||
console.log(selector);
|
|
||||||
if (selector.length && $(".course-details-page").length) {
|
if (selector.length && $(".course-details-page").length) {
|
||||||
expand_for_course_details(selector);
|
expand_for_course_details(selector);
|
||||||
} else if ($(".active-lesson").length) {
|
} else if ($(".active-lesson").length) {
|
||||||
|
|||||||
75
lms/www/billing/billing.html
Normal file
75
lms/www/billing/billing.html
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
{% extends "lms/templates/lms_base.html" %}
|
||||||
|
{% block title %}
|
||||||
|
{{ course.title if course.title else _("New Course") }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block page_content %}
|
||||||
|
<div class="common-page-style">
|
||||||
|
<div class="container form-width common-card-style column-card px-0 h-0 mt-8">
|
||||||
|
{{ Header() }}
|
||||||
|
{{ CourseDetails() }}
|
||||||
|
{{ BillingDetails() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% macro Header() %}
|
||||||
|
<div class="px-4">
|
||||||
|
<div class="page-title">
|
||||||
|
{{ _("Order Details") }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ _("Enter the billing information and complete the payment to purchase this course.") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro CourseDetails() %}
|
||||||
|
<div class="px-4 pt-10 pb-8 border-bottom">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div class="">
|
||||||
|
{{ _("Course:") }}
|
||||||
|
</div>
|
||||||
|
<div class="field-label">
|
||||||
|
{{ course.title }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-divider my-2"></div>
|
||||||
|
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div class="">
|
||||||
|
{{ _("Total Price:") }}
|
||||||
|
</div>
|
||||||
|
<div class="field-label">
|
||||||
|
<!-- $ 50k -->
|
||||||
|
{{ frappe.utils.fmt_money(course.course_price, 2, course.currency) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro BillingDetails() %}
|
||||||
|
<div class="mt-8 px-4">
|
||||||
|
<div class="bold-heading mb-4">
|
||||||
|
{{ _("Billing Details") }}
|
||||||
|
</div>
|
||||||
|
<div id="billing-form"></div>
|
||||||
|
<button class="btn btn-primary btn-md btn-pay" data-course="{{ course.name | urlencode }}">
|
||||||
|
{{ "Proceed to Payment" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{%- block script %}
|
||||||
|
{{ super() }}
|
||||||
|
<script src="https://checkout.razorpay.com/v1/checkout.js"></script>
|
||||||
|
<script>
|
||||||
|
frappe.boot.user = {
|
||||||
|
"can_create": [],
|
||||||
|
"can_select": ["Country"],
|
||||||
|
"can_read": ["Country"]
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
105
lms/www/billing/billing.js
Normal file
105
lms/www/billing/billing.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
frappe.ready(() => {
|
||||||
|
if ($("#billing-form").length) {
|
||||||
|
setup_billing();
|
||||||
|
}
|
||||||
|
|
||||||
|
$(".btn-pay").click((e) => {
|
||||||
|
generate_payment_link(e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const setup_billing = () => {
|
||||||
|
this.billing = new frappe.ui.FieldGroup({
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
fieldtype: "Data",
|
||||||
|
label: __("Address Line 1"),
|
||||||
|
fieldname: "address_line1",
|
||||||
|
reqd: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Data",
|
||||||
|
label: __("Address Line 2"),
|
||||||
|
fieldname: "address_line2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Data",
|
||||||
|
label: __("City/Town"),
|
||||||
|
fieldname: "city",
|
||||||
|
reqd: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Data",
|
||||||
|
label: __("State/Province"),
|
||||||
|
fieldname: "state",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Column Break",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Link",
|
||||||
|
label: __("Country"),
|
||||||
|
fieldname: "country",
|
||||||
|
options: "Country",
|
||||||
|
reqd: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Data",
|
||||||
|
label: __("Postal Code"),
|
||||||
|
fieldname: "pincode",
|
||||||
|
reqd: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Data",
|
||||||
|
label: __("Phone Number"),
|
||||||
|
fieldname: "phone",
|
||||||
|
reqd: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
body: $("#billing-form").get(0),
|
||||||
|
});
|
||||||
|
this.billing.make();
|
||||||
|
$("#billing-form .form-section:last").removeClass("empty-section");
|
||||||
|
$("#billing-form .frappe-control").removeClass("hide-control");
|
||||||
|
$("#billing-form .form-column").addClass("p-0");
|
||||||
|
};
|
||||||
|
|
||||||
|
const generate_payment_link = (e) => {
|
||||||
|
address = this.billing.get_values();
|
||||||
|
let course = decodeURIComponent($(e.currentTarget).attr("data-course"));
|
||||||
|
|
||||||
|
frappe.call({
|
||||||
|
method: "lms.lms.doctype.lms_course.lms_course.get_payment_options",
|
||||||
|
args: {
|
||||||
|
course: course,
|
||||||
|
phone: address.phone,
|
||||||
|
},
|
||||||
|
callback: (data) => {
|
||||||
|
data.message.handler = (response) => {
|
||||||
|
handle_success(
|
||||||
|
response,
|
||||||
|
course,
|
||||||
|
address,
|
||||||
|
data.message.order_id
|
||||||
|
);
|
||||||
|
};
|
||||||
|
let rzp1 = new Razorpay(data.message);
|
||||||
|
rzp1.open();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handle_success = (response, course, address, order_id) => {
|
||||||
|
frappe.call({
|
||||||
|
method: "lms.lms.doctype.lms_course.lms_course.verify_payment",
|
||||||
|
args: {
|
||||||
|
response: response,
|
||||||
|
course: course,
|
||||||
|
address: address,
|
||||||
|
order_id: order_id,
|
||||||
|
},
|
||||||
|
callback: (data) => {
|
||||||
|
window.location.href = data.message;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
23
lms/www/billing/billing.py
Normal file
23
lms/www/billing/billing.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
|
||||||
|
|
||||||
|
def get_context(context):
|
||||||
|
course_name = frappe.form_dict.course
|
||||||
|
|
||||||
|
if not course_name:
|
||||||
|
raise ValueError(_("Course is required."))
|
||||||
|
|
||||||
|
if frappe.session.user == "Guest":
|
||||||
|
raise frappe.PermissionError(_("You are not allowed to access this page."))
|
||||||
|
|
||||||
|
membership = frappe.db.exists(
|
||||||
|
"LMS Batch Membership", {"member": frappe.session.user, "course": course_name}
|
||||||
|
)
|
||||||
|
|
||||||
|
if membership:
|
||||||
|
raise frappe.PermissionError(_("You are already enrolled for this course"))
|
||||||
|
|
||||||
|
context.course = frappe.db.get_value(
|
||||||
|
"LMS Course", course_name, ["title", "name", "course_price", "currency"], as_dict=True
|
||||||
|
)
|
||||||
@@ -222,9 +222,9 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
{% elif course.paid_course %}
|
{% elif course.paid_course %}
|
||||||
<div class="btn btn-primary wide-button" id="buy-course" data-course="{{ course.name | urlencode }}">
|
<a class="btn btn-primary wide-button" href="/billing/{{ course.name | urlencode }}">
|
||||||
{{ _("Buy This Course") }}
|
{{ _("Buy This Course") }}
|
||||||
</div>
|
</a>
|
||||||
|
|
||||||
{% elif show_start_learing_cta(course, membership) %}
|
{% elif show_start_learing_cta(course, membership) %}
|
||||||
<div class="btn btn-primary wide-button enroll-in-course" data-course="{{ course.name | urlencode }}">
|
<div class="btn btn-primary wide-button enroll-in-course" data-course="{{ course.name | urlencode }}">
|
||||||
@@ -257,9 +257,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
@@ -280,9 +277,4 @@
|
|||||||
{{ _("You have exceeded the maximum number of attempts allowed to appear for evaluations of this course.") }}
|
{{ _("You have exceeded the maximum number of attempts allowed to appear for evaluations of this course.") }}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{%- block script %}
|
|
||||||
{{ super() }}
|
|
||||||
<script src="https://checkout.razorpay.com/v1/checkout.js"></script>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -20,10 +20,6 @@ frappe.ready(() => {
|
|||||||
$("#submit-for-review").click((e) => {
|
$("#submit-for-review").click((e) => {
|
||||||
submit_for_review(e);
|
submit_for_review(e);
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#buy-course").click((e) => {
|
|
||||||
generate_checkout_link(e);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const hide_wrapped_mentor_cards = () => {
|
const hide_wrapped_mentor_cards = () => {
|
||||||
@@ -146,24 +142,3 @@ const submit_for_review = (e) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
generate_checkout_link = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
let course = decodeURIComponent($(e.currentTarget).attr("data-course"));
|
|
||||||
|
|
||||||
if (frappe.session.user == "Guest") {
|
|
||||||
window.location.href = `/login?redirect-to=/courses/${course}`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
frappe.call({
|
|
||||||
method: "lms.lms.doctype.lms_course.lms_course.get_payment_options",
|
|
||||||
args: {
|
|
||||||
course: course,
|
|
||||||
},
|
|
||||||
callback: (data) => {
|
|
||||||
let rzp1 = new Razorpay(data.message);
|
|
||||||
rzp1.open();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
Reference in New Issue
Block a user