Browse Source

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

* 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
develop
Rucha Mahabal 3 years ago
committed by GitHub
parent
commit
57514f7b1d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      erpnext/non_profit/doctype/member/member.json
  2. 4
      erpnext/non_profit/doctype/member/member.py
  3. 92
      erpnext/non_profit/doctype/membership/membership.py
  4. 56
      erpnext/non_profit/doctype/membership/test_membership.py
  5. 1
      erpnext/patches.txt
  6. 9
      erpnext/patches/v13_0/update_subscription_status_in_memberships.py

16
erpnext/non_profit/doctype/member/member.json

@ -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",

4
erpnext/non_profit/doctype/member/member.py

@ -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)

92
erpnext/non_profit/doctype/membership/membership.py

@ -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()
@ -294,9 +286,67 @@ def trigger_razorpay_subscription(*args, **kwargs):
message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), _("Payment ID"), payment.id)
log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name))
notify_failure(log)
return { "status": "Failed", "reason": e}
return {"status": "Failed", "reason": e}
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 { "status": "Success" }
return data
def get_company_for_memberships():
@ -362,4 +412,4 @@ def set_expired_status():
`tabMembership` SET `status` = 'Expired'
WHERE
`status` not in ('Cancelled') AND `to_date` < %s
""", (nowdate()))
""", (nowdate()))

56
erpnext/non_profit/doctype/membership/test_membership.py

@ -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)
@ -115,4 +135,28 @@ def setup_membership():
else:
plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm")
return plan
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"
},
}
}
}
}

1
erpnext/patches.txt

@ -292,3 +292,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

9
erpnext/patches/v13_0/update_subscription_status_in_memberships.py

@ -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')
Loading…
Cancel
Save