refactor: razorpay subscription for memberships

This commit is contained in:
Shivam Mishra 2020-04-07 14:49:28 +05:30
parent 8d6cded07f
commit 65e11a9640
4 changed files with 208 additions and 168 deletions

View File

@ -2,6 +2,14 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Member', { frappe.ui.form.on('Member', {
setup: function(frm) {
frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => {
if (val && (frm.doc.subscription_id || frm.doc.customer_id)) {
frm.set_df_property('razorpay_details_section', 'hidden', false);
}
})
},
refresh: function(frm) { refresh: function(frm) {
frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Member'}; frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Member'};

View File

@ -23,7 +23,14 @@
"address_contacts", "address_contacts",
"address_html", "address_html",
"column_break_9", "column_break_9",
"contact_html" "contact_html",
"razorpay_details_section",
"subscription_id",
"customer_id",
"subscription_activated",
"column_break_21",
"subscription_start",
"subscription_end"
], ],
"fields": [ "fields": [
{ {
@ -127,11 +134,49 @@
"fieldname": "email_id", "fieldname": "email_id",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Email Address" "label": "Email Address"
},
{
"fieldname": "subscription_id",
"fieldtype": "Data",
"label": "Subscription ID",
"read_only": 1
},
{
"fieldname": "customer_id",
"fieldtype": "Data",
"label": "Customer ID",
"read_only": 1
},
{
"fieldname": "razorpay_details_section",
"fieldtype": "Section Break",
"hidden": 1,
"label": "Razorpay Details"
},
{
"fieldname": "column_break_21",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "subscription_activated",
"fieldtype": "Check",
"label": "Subscription Activated"
},
{
"fieldname": "subscription_start",
"fieldtype": "Date",
"label": "Subscription Start "
},
{
"fieldname": "subscription_end",
"fieldtype": "Date",
"label": "Subscription End"
} }
], ],
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2020-03-30 13:42:06.488851", "modified": "2020-04-07 14:20:33.215700",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Non Profit", "module": "Non Profit",
"name": "Member", "name": "Member",

View File

@ -3,9 +3,12 @@
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.contacts.address_and_contact import load_address_and_contact from frappe.contacts.address_and_contact import load_address_and_contact
from frappe.utils import cint
from frappe.integrations.utils import get_payment_gateway_controller
class Member(Document): class Member(Document):
def onload(self): def onload(self):
@ -21,4 +24,107 @@ class Member(Document):
def validate_email_type(self, email): def validate_email_type(self, email):
from frappe.utils import validate_email_address from frappe.utils import validate_email_address
validate_email_address(email.strip(), True) validate_email_address(email.strip(), True)
def setup_subscription(self):
membership_settings = frappe.get_doc("Membership Settings")
if not membership_settings.enable_razorpay:
frappe.throw("Please enable Razorpay to setup subscription")
controller = get_payment_gateway_controller("Razorpay")
settings = controller.get_settings({})
plan_id = frappe.get_value("Membership Type", self.membership_type, "razorpay_plan_id")
if not plan_id:
frappe.throw(_("Please setup Razorpay Plan ID"))
subscription_details = {
"plan_id": plan_id,
"billing_frequency": cint(membership_settings.billing_frequency),
"customer_notify": 1
}
args = {
'subscription_details': subscription_details
}
subscription = controller.setup_subscription(settings, **args)
return subscription
def get_or_create_member(user_details):
member_list = frappe.get_all("Member", filters={'email': user_details.email, 'membership_type': user_details.plan_id})
if member_list and member_list[0]:
return member_list[0]['name']
else:
return create_member(user_details)
def create_member(user_details):
member = frappe.new_doc("Member")
member.update({
"member_name": user_details.fullname,
"email_id": user_details.email,
"pan_number": user_details.pan,
"membership_type": user_details.plan_id,
"customer": create_customer(user_details)
})
member.insert(ignore_permissions=True)
return member
def create_customer(user_details):
customer = frappe.new_doc("Customer")
customer.customer_name = user_details.fullname
customer.customer_type = "Individual"
customer.insert(ignore_permissions=True)
try:
contact = frappe.new_doc("Contact")
contact.first_name = user_details.fullname
contact.add_phone(user_details.mobile, is_primary_phone=1, is_primary_mobile_no=1)
contact.add_email(user_details.email, is_primary=1)
contact.insert(ignore_permissions=True)
contact.append("links", {
"link_doctype": "Customer",
"link_name": customer.name
})
contact.insert()
except Exception:
error_log = frappe.log_error(frappe.get_traceback(), _("Contact Creation Failed"))
return customer.name
@frappe.whitelist(allow_guest=True)
def create_member_subscription_order(user_details):
"""Summary
Args:
user_details (TYPE): Description
Returns:
Dictionary: Dictionary with subscription details
{
'subscription_details': {
'plan_id': 'plan_EXwyxDYDCj3X4v',
'billing_frequency': 24,
'customer_notify': 1
},
'subscription_id': 'sub_EZycCvXFvqnC6p'
}
"""
# {"plan_id":"IFF Starter","fullname":"Shivam Mishra","mobile":"7506056962","email":"shivam@shivam.dev","pan":"Testing123"}
user_details = frappe._dict(user_details)
plan = frappe.get_doc("Membership Type", user_details.plan_id)
member = get_or_create_member(user_details)
if not member:
member = create_member(user_details)
subscription = member.setup_subscription()
member.subscription_id = subscription.get('subscription_id')
member.save(ignore_permissions=True)
return subscription

View File

@ -4,11 +4,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import json import json
from datetime import datetime
import frappe import frappe
import six
from datetime import datetime
from frappe.model.document import Document from frappe.model.document import Document
from frappe.email import sendmail_to_system_managers
from frappe.utils import add_days, add_years, nowdate, getdate, add_months, cint from frappe.utils import add_days, add_years, nowdate, getdate, add_months, cint
from frappe.integrations.utils import get_payment_gateway_controller
from frappe import _ from frappe import _
import erpnext import erpnext
@ -56,139 +57,16 @@ class Membership(Document):
self.load_from_db() self.load_from_db()
self.db_set('paid', 1) self.db_set('paid', 1)
def setup_subscription(self): def get_member_based_on_subscription(subscription_id, email):
membership_settings = frappe.get_doc("Membership Settings") members = frappe.get_all("Member", filters={
if not membership_settings.enable_razorpay: 'subscription_id': subscription_id,
frappe.throw("Please enable Razorpay to setup subscription") 'email_id': email
}, order_by="creation desc")
controller = get_payment_gateway_controller("Razorpay") return frappe.get_doc("Member", members[0]['name'])
settings = controller.get_settings({})
plan_id = frappe.get_value("Membership Type", self.membership_type, "razorpay_plan_id")
if not plan_id:
frappe.throw(_("Please setup Razorpay Plan ID"))
subscription_details = {
"plan_id": plan_id,
"billing_frequency": cint(membership_settings.billing_frequency),
"customer_notify": 1
}
args = {
'subscription_details': subscription_details
}
subscription = controller.setup_subscription(settings, **args)
return subscription
def get_member_if_exists(email, plan): @frappe.whitelist()
member_list = frappe.get_all("Member", filters={'email': email, 'membership_type': plan})
if member_list and member_list[0]:
return member_list[0]['name']
else:
return None
def create_member(user_details):
member = frappe.new_doc("Member")
member.update({
"member_name": user_details.fullname,
"email_id": user_details.email,
"pan_number": user_details.pan,
"membership_type": user_details.plan_id,
"customer": create_customer(user_details)
})
member.insert(ignore_permissions=True)
return member
def create_customer(user_details):
customer = frappe.new_doc("Customer")
customer.customer_name = user_details.fullname
customer.customer_type = "Individual"
customer.insert(ignore_permissions=True)
try:
contact = frappe.new_doc("Contact")
contact.first_name = user_details.fullname
contact.add_phone(user_details.mobile, is_primary_phone=1, is_primary_mobile_no=1)
contact.add_email(user_details.email, is_primary=1)
contact.insert(ignore_permissions=True)
contact.append("links", {
"link_doctype": "Customer",
"link_name": customer.name
})
contact.insert()
except Exception:
error_log = frappe.log_error(frappe.get_traceback(), _("Contact Creation Failed"))
return customer.name
def create_membership(member, plan):
membership = frappe.new_doc("Membership")
membership.update({
"member": member.name,
"membership_status": "New",
"membership_type": member.membership_type,
"currency": "INR",
"amount": plan.amount,
"from_date": getdate()
})
membership.insert(ignore_permissions=True)
return membership
@frappe.whitelist(allow_guest=True)
def create_membership_subscription(user_details):
"""Summary
Args:
user_details (TYPE): Description
Returns:
Dictionary: Dictionary with subscription details
{
'subscription_details': {
'plan_id': 'plan_EXwyxDYDCj3X4v',
'billing_frequency': 24,
'customer_notify': 1
},
'subscription_id': 'sub_EZycCvXFvqnC6p'
}
"""
# {"plan_id":"IFF Starter","fullname":"Shivam Mishra","mobile":"7506056962","email":"shivam@shivam.dev","pan":"Testing123"}
user_details = frappe._dict(user_details)
member = get_member_if_exists(user_details.email, user_details.plan_id)
plan = frappe.get_doc("Membership Type", user_details.plan_id)
if not member:
member = create_member(user_details)
membership = create_membership(member, plan)
subscription = membership.setup_subscription()
membership.subscription_id = subscription.get('subscription_id')
membership.save(ignore_permissions=True)
return subscription
def get_membership_based_on_subscription(subscription_id, custom_filters={}):
filters = {'subscription_id': subscription.id}
filters.update(custom_filters)
memberships = frappe.get_all("Membership", filters=filters, order_by="creation")
if not memberships:
return None
return frappe.get_doc("Membership", memberships[0]['name'])
@frappe.whitelist(allow_guest=True)
def trigger_razorpay_subscription(data): def trigger_razorpay_subscription(data):
if isinstance(data, six.string_types): if isinstance(data, six.string_types):
data = json.loads(data) data = json.loads(data)
@ -200,49 +78,52 @@ def trigger_razorpay_subscription(data):
payment = data.payload.get("payment", {}).get('entity', {}) payment = data.payload.get("payment", {}).get('entity', {})
payment = frappe._dict(payment) payment = frappe._dict(payment)
try:
data_json = json.dumps(data, indent=4, sort_keys=True)
member = get_member_based_on_subscription(subscription.id, payment.email)
except Exception as e:
error_log = frappe.log_error(frappe.get_traceback() + '\n' + data_json , _("Membership Webhook Failed"))
notify_failure(log)
raise e
if data.event == "subscription.activated": if data.event == "subscription.activated":
membership = get_membership_based_on_subscription(subscription.id, {"membership_status": "New"}) member.customer_id = payment.customer_id
else member.subscription_start = datetime.fromtimestamp(subscription.start_at)
prev_membership = get_membership_based_on_subscription(subscription.id, {"payment_id": payment.id, "paid": 1}) member.subscription_end = datetime.fromtimestamp(subscription.end_at)
if prev_membership: member.subscription_activated = 1
print("payment already done") member.save(ignore_permissions=True)
return elif data.event == "subscription.charged":
prev_membership = get_membership_based_on_subscription(subscription.id)
membership = frappe.new_doc("Membership") membership = frappe.new_doc("Membership")
membership.update({ membership.update({
"member": prev_membership.member, "member": member.name,
"membership_status": "Current", "membership_status": "Current",
"membership_type": prev_membership.membership_type, "membership_type": member.membership_type,
"currency": "INR", "currency": "INR",
"paid": 1,
"payment_id": payment.id,
"webhook_payload": data_json,
"from_date": datetime.fromtimestamp(subscription.current_start),
"to_date": datetime.fromtimestamp(subscription.current_end),
"amount": payment.amount / 100 # Convert to rupees from paise
}) })
membership.insert(ignore_permissions=True)
subscription_charged(subscription, payment, membership)
def subscription_charged(subscription, payment, membership=None):
data = {
"subscription": subscription,
"payment": payment,
}
membership.paid = 1
membership.payment_id = payment.id
membership.webhook_payload = json.dumps(data, indent=4, sort_keys=True)
membership.from_date = datetime.fromtimestamp(subscription.current_start)
membership.to_date = datetime.fromtimestamp(subscription.current_end)
membership.amount = payment.amount / 100 # Convert to rupees from paise
if membership.is_new():
membership.insert()
else:
membership.save()
return True return True
def notify_failure(log):
try:
content = """Dear System Manager,
Razorpay webhook for creating renewing membership subscription failed due to some reason. Please check the following error log linked below
Error Log: {0}
Regards,
Administrator""".format(get_link_to_form("Error Log", log.name))
sendmail_to_system_managers("[Important] [ERPNext] Razorpay membership webhook failed , please check.", content)
except:
pass