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

@@ -9,14 +9,15 @@ from frappe.utils import nowdate
class LMSCoupon(Document):
def validate(self):
# Normalize code to uppercase and strip spaces
if self.code:
self.code = self.code.strip().upper()
if not self.code:
frappe.throw(_("Coupon code is required."))
frappe.throw(_("Coupon code is required"))
if len(self.code) < 6:
frappe.throw(_("Coupon code must be atleast 6 characters"))
# Ensure uniqueness of code (case-insensitive)
if self.name:
existing = frappe.db.exists(
"LMS Coupon",
@@ -27,30 +28,49 @@ class LMSCoupon(Document):
)
else:
existing = frappe.db.exists("LMS Coupon", {"code": self.code})
if existing:
frappe.throw(_("Coupon code already exists."))
frappe.throw(_("Coupon code is already taken. Use a different one"))
if not self.discount_type:
frappe.throw(_("Discount type is required."))
frappe.throw(_("Discount type is required"))
if self.discount_type == "Percent":
if self.percent_off is None:
frappe.throw(_("Percent Off is required for Percent discount type."))
if not (0 < float(self.percent_off) <= 100):
frappe.throw(_("Percent Off must be between 1 and 100."))
# Clear the other field to avoid confusion
if not self.percent_off or self.percent_off == "":
frappe.throw(_("Discount percentage is required"))
try:
percent_value = float(self.percent_off)
if not (0 < percent_value <= 100):
frappe.throw(_("Discount percentage must be between 1 and 100"))
except (ValueError, TypeError):
frappe.throw(_("Discount percentage must be a valid number"))
self.amount_off = None
if self.discount_type == "Amount":
if self.amount_off is None:
frappe.throw(_("Amount Off is required for Amount discount type."))
if float(self.amount_off) < 0:
frappe.throw(_("Amount Off cannot be negative."))
# Clear the other field
if not self.amount_off or self.amount_off == "":
frappe.throw(_("Discount amount is required"))
try:
amount_value = float(self.amount_off)
if amount_value < 0:
frappe.throw(_("Discount amount cannot be negative"))
except (ValueError, TypeError):
frappe.throw(_("Discount amount must be a valid number"))
self.percent_off = None
if self.usage_limit is not None and int(self.usage_limit) < 0:
frappe.throw(_("Usage limit cannot be negative."))
if self.usage_limit is not None and self.usage_limit != "":
try:
usage_value = int(self.usage_limit)
if usage_value < 0:
frappe.throw(_("Usage limit cannot be negative"))
except (ValueError, TypeError):
frappe.throw(_("Usage limit must be a valid number"))
if self.expires_on and str(self.expires_on) < nowdate():
frappe.throw(_("Expiry date cannot be in the past."))
frappe.throw(_("Expiry date cannot be in the past"))
if not self.get("applicable_items") or len(self.get("applicable_items")) == 0:
frappe.throw(_("Please select atleast one course or batch"))
for item in self.get("applicable_items"):
if not item.get("reference_name"):
frappe.throw(_("Please select a valid course or batch"))

View File

@@ -19,10 +19,9 @@
"currency",
"amount",
"coupon",
"discount_type",
"discount_percent",
"discount_amount",
"amount_with_gst",
"gst_amount",
"total_amount",
"column_break_yxpl",
"order_id",
"payment_id",
@@ -57,17 +56,6 @@
"label": "Coupon",
"options": "LMS Coupon"
},
{
"fieldname": "discount_type",
"fieldtype": "Select",
"label": "Discount Type",
"options": "\nPercent\nAmount"
},
{
"fieldname": "discount_percent",
"fieldtype": "Percent",
"label": "Discount Percent"
},
{
"fieldname": "discount_amount",
"fieldtype": "Currency",
@@ -144,12 +132,6 @@
"options": "User",
"reqd": 1
},
{
"depends_on": "eval:doc.currency == \"INR\";",
"fieldname": "amount_with_gst",
"fieldtype": "Currency",
"label": "Amount with GST"
},
{
"fieldname": "payment_for_document_type",
"fieldtype": "Select",
@@ -176,6 +158,21 @@
"fieldtype": "Check",
"in_standard_filter": 1,
"label": "Payment for Certificate"
},
{
"depends_on": "eval:doc.currency == \"INR\";",
"fieldname": "gst_amount",
"fieldtype": "Currency",
"label": "GST Amount",
"options": "currency"
},
{
"fieldname": "total_amount",
"fieldtype": "Currency",
"label": "Total Amount",
"options": "currency",
"read_only": 1,
"reqd": 1
}
],
"index_web_pages_for_search": 1,
@@ -189,8 +186,8 @@
"link_fieldname": "payment"
}
],
"modified": "2025-09-23 11:04:00.462274",
"modified_by": "sayali@frappe.io",
"modified": "2025-10-13 15:25:56.127625",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Payment",
"owner": "Administrator",

View File

@@ -5,12 +5,15 @@ import frappe
from frappe import _
from frappe.email.doctype.email_template.email_template import get_email_template
from frappe.model.document import Document
from frappe.utils import add_days, nowdate
from frappe.utils import add_days, nowdate, flt
class LMSPayment(Document):
pass
def validate(self):
amount = flt(self.amount or 0, self.precision("amount"))
discount = flt(self.discount_amount or 0, self.precision("discount_amount"))
gst = flt(self.gst_amount or 0, self.precision("gst_amount"))
self.total_amount = flt(amount - discount + gst, self.precision("total_amount"))
def send_payment_reminder():
outgoing_email_account = frappe.get_cached_value(

View File

@@ -1,6 +1,5 @@
import frappe
def get_payment_gateway():
return frappe.db.get_single_value("LMS Settings", "payment_gateway")
@@ -19,49 +18,46 @@ def validate_currency(payment_gateway, currency):
@frappe.whitelist()
def get_payment_link(
doctype,
docname,
title,
amount,
total_amount,
currency,
address,
redirect_to,
payment_for_certificate,
coupon_code=None,
doctype,
docname,
title,
amount,
discount_amount,
gst_amount,
currency,
address,
redirect_to,
payment_for_certificate,
coupon_code=None,
):
payment_gateway = get_payment_gateway()
address = frappe._dict(address)
amount_with_gst = total_amount if total_amount != amount else 0
coupon_context = None
# Coupon application only for courses/batches
if doctype in ["LMS Course", "LMS Batch"] and coupon_code:
try:
from lms.lms.utils import apply_coupon
coupon_context = None
if doctype in ["LMS Course", "LMS Batch"] and coupon_code:
try:
from lms.lms.utils import apply_coupon
coupon_context = apply_coupon(doctype, docname, coupon_code)
except Exception:
pass
applied = apply_coupon(doctype, docname, coupon_code)
# Override total_amount based on validated coupon calculation
total_amount = applied.get("amount", total_amount)
coupon_context = applied
except Exception:
# Ignore coupon errors here; frontend handles validation
pass
payment = record_payment(
address,
doctype,
docname,
amount,
currency,
amount_with_gst,
payment_for_certificate,
coupon_context,
)
payment = record_payment(
address,
doctype,
docname,
amount,
currency,
discount_amount,
gst_amount,
payment_for_certificate,
coupon_context,
)
controller = get_controller(payment_gateway)
payment_details = {
"amount": total_amount,
"amount": amount - discount_amount + gst_amount,
"discount_amount": discount_amount,
"gst_amount": gst_amount,
"title": f"Payment for {doctype} {title} {docname}",
"description": f"{address.billing_name}'s payment for {title}",
"reference_doctype": doctype,
@@ -83,14 +79,15 @@ def get_payment_link(
def record_payment(
address,
doctype,
docname,
amount,
currency,
amount_with_gst=0,
payment_for_certificate=0,
coupon_context=None,
address,
doctype,
docname,
amount,
currency,
discount_amount=0,
gst_amount=0,
payment_for_certificate=0,
coupon_context=None,
):
address = frappe._dict(address)
address_name = save_address(address)
@@ -103,7 +100,8 @@ def record_payment(
"address": address_name,
"amount": amount,
"currency": currency,
"amount_with_gst": amount_with_gst,
"discount_amount": discount_amount,
"gst_amount": gst_amount,
"gstin": address.gstin,
"pan": address.pan,
"source": address.source,
@@ -112,15 +110,15 @@ def record_payment(
"payment_for_certificate": payment_for_certificate,
}
)
if coupon_context:
payment_doc.update(
{
"coupon": coupon_context.get("coupon"),
"discount_type": coupon_context.get("discount_type"),
"discount_percent": coupon_context.get("discount_percent"),
"discount_amount": coupon_context.get("discount_amount"),
}
)
if coupon_context:
payment_doc.update(
{
"coupon": coupon_context.get("coupon"),
"discount_type": coupon_context.get("discount_type"),
"discount_percent": coupon_context.get("discount_percent"),
"discount_amount": coupon_context.get("discount_amount"),
}
)
payment_doc.save(ignore_permissions=True)
return payment_doc