Skip to content

Commit

Permalink
feat(Non Profit): API Endpoint to update halted Razorpay subscriptions (
Browse files Browse the repository at this point in the history
frappe#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
  • Loading branch information
ruchamahabal committed Jul 20, 2021
1 parent 41705ac commit 9a5b056
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 36 deletions.
16 changes: 8 additions & 8 deletions erpnext/non_profit/doctype/member/member.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"razorpay_details_section",
"subscription_id",
"customer_id",
"subscription_activated",
"subscription_status",
"column_break_21",
"subscription_start",
"subscription_end"
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion erpnext/non_profit/doctype/member/member.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
92 changes: 71 additions & 21 deletions erpnext/non_profit/doctype/membership/membership.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand All @@ -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")
Expand All @@ -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)
Expand Down Expand Up @@ -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()

Expand All @@ -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():
Expand Down Expand Up @@ -362,4 +412,4 @@ def set_expired_status():
`tabMembership` SET `status` = 'Expired'
WHERE
`status` not in ('Cancelled') AND `to_date` < %s
""", (nowdate()))
""", (nowdate()))
56 changes: 50 additions & 6 deletions erpnext/non_profit/doctype/membership/test_membership.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,24 @@
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):
def setUp(self):
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

Expand Down Expand Up @@ -51,6 +57,20 @@ def test_renew_within_30_days(self):
"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)

Expand Down Expand Up @@ -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 change: 1 addition & 0 deletions erpnext/patches.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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')

0 comments on commit 9a5b056

Please sign in to comment.