feat(Non Profit): API Endpoint to update halted Razorpay subscriptions (#26427)

* feat: Update Subscription Activated field to Subscription Status to accomodate Halted status

* feat: API Endpoint to halt Razorpay subscription

* fix: sider

* fix: validation message

* test: halted razorpay subscription
This commit is contained in:
Rucha Mahabal 2021-07-21 00:24:09 +05:30 committed by GitHub
parent 013b352639
commit a758071532
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 142 additions and 36 deletions

View File

@ -26,7 +26,7 @@
"razorpay_details_section",
"subscription_id",
"customer_id",
"subscription_activated",
"subscription_status",
"column_break_21",
"subscription_start",
"subscription_end"
@ -151,12 +151,6 @@
"fieldname": "column_break_21",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "subscription_activated",
"fieldtype": "Check",
"label": "Subscription Activated"
},
{
"fieldname": "subscription_start",
"fieldtype": "Date",
@ -166,11 +160,17 @@
"fieldname": "subscription_end",
"fieldtype": "Date",
"label": "Subscription End"
},
{
"fieldname": "subscription_status",
"fieldtype": "Select",
"label": "Subscription Status",
"options": "\nActive\nHalted"
}
],
"image_field": "image",
"links": [],
"modified": "2020-11-09 12:12:10.174647",
"modified": "2021-07-11 14:27:26.368039",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Member",

View File

@ -84,7 +84,9 @@ def create_member(user_details):
"email_id": user_details.email,
"pan_number": user_details.pan or None,
"membership_type": user_details.plan_id,
"subscription_id": user_details.subscription_id or None
"customer_id": user_details.customer_id or None,
"subscription_id": user_details.subscription_id or None,
"subscription_status": user_details.subscription_status or ""
})
member.insert(ignore_permissions=True)

View File

@ -196,11 +196,14 @@ def make_invoice(membership, member, plan, settings):
return invoice
def get_member_based_on_subscription(subscription_id, email):
members = frappe.get_all("Member", filters={
"subscription_id": subscription_id,
"email_id": email
}, order_by="creation desc")
def get_member_based_on_subscription(subscription_id, email=None, customer_id=None):
filters = {"subscription_id": subscription_id}
if email:
filters.update({"email_id": email})
if customer_id:
filters.update({"customer_id": customer_id})
members = frappe.get_all("Member", filters=filters, order_by="creation desc")
try:
return frappe.get_doc("Member", members[0]["name"])
@ -209,8 +212,6 @@ def get_member_based_on_subscription(subscription_id, email):
def verify_signature(data, endpoint="Membership"):
if frappe.flags.in_test or os.environ.get("CI"):
return True
signature = frappe.request.headers.get("X-Razorpay-Signature")
settings = frappe.get_doc("Non Profit Settings")
@ -225,16 +226,7 @@ def verify_signature(data, endpoint="Membership"):
@frappe.whitelist(allow_guest=True)
def trigger_razorpay_subscription(*args, **kwargs):
data = frappe.request.get_data(as_text=True)
try:
verify_signature(data)
except Exception as e:
log = frappe.log_error(e, "Membership Webhook Verification Error")
notify_failure(log)
return { "status": "Failed", "reason": e}
if isinstance(data, six.string_types):
data = json.loads(data)
data = frappe._dict(data)
data = process_request_data(data)
subscription = data.payload.get("subscription", {}).get("entity", {})
subscription = frappe._dict(subscription)
@ -281,7 +273,7 @@ def trigger_razorpay_subscription(*args, **kwargs):
# Update membership values
member.subscription_start = datetime.fromtimestamp(subscription.start_at)
member.subscription_end = datetime.fromtimestamp(subscription.end_at)
member.subscription_activated = 1
member.subscription_status = "Active"
member.flags.ignore_mandatory = True
member.save()
@ -299,6 +291,64 @@ def trigger_razorpay_subscription(*args, **kwargs):
return {"status": "Success"}
@frappe.whitelist(allow_guest=True)
def update_halted_razorpay_subscription(*args, **kwargs):
"""
When all retries have been exhausted, Razorpay moves the subscription to the halted state.
The customer has to manually retry the charge or change the card linked to the subscription,
for the subscription to move back to the active state.
"""
if frappe.request:
data = frappe.request.get_data(as_text=True)
data = process_request_data(data)
elif frappe.flags.in_test:
data = kwargs.get("data")
data = frappe._dict(data)
else:
return
if not data.event == "subscription.halted":
return
subscription = data.payload.get("subscription", {}).get("entity", {})
subscription = frappe._dict(subscription)
try:
member = get_member_based_on_subscription(subscription.id, customer_id=subscription.customer_id)
if not member:
frappe.throw(_("Member with Razorpay Subscription ID {0} not found").format(subscription.id))
member.subscription_status = "Halted"
member.flags.ignore_mandatory = True
member.save()
if subscription.get("notes"):
member = get_additional_notes(member, subscription)
except Exception as e:
message = "{0}\n\n{1}".format(e, frappe.get_traceback())
log = frappe.log_error(message, _("Error updating halted status for member {0}").format(member.name))
notify_failure(log)
return {"status": "Failed", "reason": e}
return {"status": "Success"}
def process_request_data(data):
try:
verify_signature(data)
except Exception as e:
log = frappe.log_error(e, "Membership Webhook Verification Error")
notify_failure(log)
return {"status": "Failed", "reason": e}
if isinstance(data, six.string_types):
data = json.loads(data)
data = frappe._dict(data)
return data
def get_company_for_memberships():
company = frappe.db.get_single_value("Non Profit Settings", "company")
if not company:

View File

@ -6,6 +6,7 @@ import unittest
import frappe
import erpnext
from erpnext.non_profit.doctype.member.member import create_member
from erpnext.non_profit.doctype.membership.membership import update_halted_razorpay_subscription
from frappe.utils import nowdate, add_months
class TestMembership(unittest.TestCase):
@ -13,11 +14,16 @@ class TestMembership(unittest.TestCase):
plan = setup_membership()
# make test member
self.member_doc = create_member(frappe._dict({
'fullname': "_Test_Member",
'email': "_test_member_erpnext@example.com",
'plan_id': plan.name
}))
self.member_doc = create_member(
frappe._dict({
"fullname": "_Test_Member",
"email": "_test_member_erpnext@example.com",
"plan_id": plan.name,
"subscription_id": "sub_DEX6xcJ1HSW4CR",
"customer_id": "cust_C0WlbKhp3aLA7W",
"subscription_status": "Active"
})
)
self.member_doc.make_customer_and_link()
self.member = self.member_doc.name
@ -51,6 +57,20 @@ class TestMembership(unittest.TestCase):
"to_date": add_months(nowdate(), 3),
})
def test_halted_memberships(self):
make_membership(self.member, {
"from_date": add_months(nowdate(), 2),
"to_date": add_months(nowdate(), 3)
})
self.assertEqual(frappe.db.get_value("Member", self.member, "subscription_status"), "Active")
payload = get_subscription_payload()
update_halted_razorpay_subscription(data=payload)
self.assertEqual(frappe.db.get_value("Member", self.member, "subscription_status"), "Halted")
def tearDown(self):
frappe.db.rollback()
def set_config(key, value):
frappe.db.set_value("Non Profit Settings", None, key, value)
@ -116,3 +136,27 @@ def setup_membership():
plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm")
return plan
def get_subscription_payload():
return {
"entity": "event",
"account_id": "acc_BFQ7uQEaa7j2z7",
"event": "subscription.halted",
"contains": [
"subscription"
],
"payload": {
"subscription": {
"entity": {
"id": "sub_DEX6xcJ1HSW4CR",
"entity": "subscription",
"plan_id": "_rzpy_test_milythm",
"customer_id": "cust_C0WlbKhp3aLA7W",
"status": "halted",
"notes": {
"Important": "Notes for Internal Reference"
},
}
}
}
}

View File

@ -295,3 +295,4 @@ erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
erpnext.patches.v13_0.update_job_card_details
erpnext.patches.v13_0.update_level_in_bom #1234sswef
erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry
erpnext.patches.v13_0.update_subscription_status_in_memberships

View File

@ -0,0 +1,9 @@
import frappe
def execute():
if frappe.db.exists('DocType', 'Member'):
frappe.reload_doc('Non Profit', 'doctype', 'Member')
if frappe.db.has_column('Member', 'subscription_activated'):
frappe.db.sql('UPDATE `tabMember` SET subscription_status = "Active" WHERE subscription_activated = 1')
frappe.db.sql_ddl('ALTER table `tabMember` DROP COLUMN subscription_activated')