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:
parent
013b352639
commit
a758071532
@ -26,7 +26,7 @@
|
|||||||
"razorpay_details_section",
|
"razorpay_details_section",
|
||||||
"subscription_id",
|
"subscription_id",
|
||||||
"customer_id",
|
"customer_id",
|
||||||
"subscription_activated",
|
"subscription_status",
|
||||||
"column_break_21",
|
"column_break_21",
|
||||||
"subscription_start",
|
"subscription_start",
|
||||||
"subscription_end"
|
"subscription_end"
|
||||||
@ -151,12 +151,6 @@
|
|||||||
"fieldname": "column_break_21",
|
"fieldname": "column_break_21",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "subscription_activated",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Subscription Activated"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "subscription_start",
|
"fieldname": "subscription_start",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
@ -166,11 +160,17 @@
|
|||||||
"fieldname": "subscription_end",
|
"fieldname": "subscription_end",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"label": "Subscription End"
|
"label": "Subscription End"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "subscription_status",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Subscription Status",
|
||||||
|
"options": "\nActive\nHalted"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-11-09 12:12:10.174647",
|
"modified": "2021-07-11 14:27:26.368039",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Non Profit",
|
"module": "Non Profit",
|
||||||
"name": "Member",
|
"name": "Member",
|
||||||
|
@ -84,7 +84,9 @@ def create_member(user_details):
|
|||||||
"email_id": user_details.email,
|
"email_id": user_details.email,
|
||||||
"pan_number": user_details.pan or None,
|
"pan_number": user_details.pan or None,
|
||||||
"membership_type": user_details.plan_id,
|
"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)
|
member.insert(ignore_permissions=True)
|
||||||
|
@ -196,11 +196,14 @@ def make_invoice(membership, member, plan, settings):
|
|||||||
return invoice
|
return invoice
|
||||||
|
|
||||||
|
|
||||||
def get_member_based_on_subscription(subscription_id, email):
|
def get_member_based_on_subscription(subscription_id, email=None, customer_id=None):
|
||||||
members = frappe.get_all("Member", filters={
|
filters = {"subscription_id": subscription_id}
|
||||||
"subscription_id": subscription_id,
|
if email:
|
||||||
"email_id": email
|
filters.update({"email_id": email})
|
||||||
}, order_by="creation desc")
|
if customer_id:
|
||||||
|
filters.update({"customer_id": customer_id})
|
||||||
|
|
||||||
|
members = frappe.get_all("Member", filters=filters, order_by="creation desc")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return frappe.get_doc("Member", members[0]["name"])
|
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"):
|
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")
|
signature = frappe.request.headers.get("X-Razorpay-Signature")
|
||||||
|
|
||||||
settings = frappe.get_doc("Non Profit Settings")
|
settings = frappe.get_doc("Non Profit Settings")
|
||||||
@ -225,16 +226,7 @@ def verify_signature(data, endpoint="Membership"):
|
|||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def trigger_razorpay_subscription(*args, **kwargs):
|
def trigger_razorpay_subscription(*args, **kwargs):
|
||||||
data = frappe.request.get_data(as_text=True)
|
data = frappe.request.get_data(as_text=True)
|
||||||
try:
|
data = process_request_data(data)
|
||||||
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)
|
|
||||||
|
|
||||||
subscription = data.payload.get("subscription", {}).get("entity", {})
|
subscription = data.payload.get("subscription", {}).get("entity", {})
|
||||||
subscription = frappe._dict(subscription)
|
subscription = frappe._dict(subscription)
|
||||||
@ -281,7 +273,7 @@ def trigger_razorpay_subscription(*args, **kwargs):
|
|||||||
# Update membership values
|
# Update membership values
|
||||||
member.subscription_start = datetime.fromtimestamp(subscription.start_at)
|
member.subscription_start = datetime.fromtimestamp(subscription.start_at)
|
||||||
member.subscription_end = datetime.fromtimestamp(subscription.end_at)
|
member.subscription_end = datetime.fromtimestamp(subscription.end_at)
|
||||||
member.subscription_activated = 1
|
member.subscription_status = "Active"
|
||||||
member.flags.ignore_mandatory = True
|
member.flags.ignore_mandatory = True
|
||||||
member.save()
|
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)
|
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))
|
log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name))
|
||||||
notify_failure(log)
|
notify_failure(log)
|
||||||
return { "status": "Failed", "reason": e}
|
return {"status": "Failed", "reason": e}
|
||||||
|
|
||||||
return { "status": "Success" }
|
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():
|
def get_company_for_memberships():
|
||||||
@ -362,4 +412,4 @@ def set_expired_status():
|
|||||||
`tabMembership` SET `status` = 'Expired'
|
`tabMembership` SET `status` = 'Expired'
|
||||||
WHERE
|
WHERE
|
||||||
`status` not in ('Cancelled') AND `to_date` < %s
|
`status` not in ('Cancelled') AND `to_date` < %s
|
||||||
""", (nowdate()))
|
""", (nowdate()))
|
||||||
|
@ -6,6 +6,7 @@ import unittest
|
|||||||
import frappe
|
import frappe
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.non_profit.doctype.member.member import create_member
|
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
|
from frappe.utils import nowdate, add_months
|
||||||
|
|
||||||
class TestMembership(unittest.TestCase):
|
class TestMembership(unittest.TestCase):
|
||||||
@ -13,11 +14,16 @@ class TestMembership(unittest.TestCase):
|
|||||||
plan = setup_membership()
|
plan = setup_membership()
|
||||||
|
|
||||||
# make test member
|
# make test member
|
||||||
self.member_doc = create_member(frappe._dict({
|
self.member_doc = create_member(
|
||||||
'fullname': "_Test_Member",
|
frappe._dict({
|
||||||
'email': "_test_member_erpnext@example.com",
|
"fullname": "_Test_Member",
|
||||||
'plan_id': plan.name
|
"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_doc.make_customer_and_link()
|
||||||
self.member = self.member_doc.name
|
self.member = self.member_doc.name
|
||||||
|
|
||||||
@ -51,6 +57,20 @@ class TestMembership(unittest.TestCase):
|
|||||||
"to_date": add_months(nowdate(), 3),
|
"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):
|
def set_config(key, value):
|
||||||
frappe.db.set_value("Non Profit Settings", None, key, value)
|
frappe.db.set_value("Non Profit Settings", None, key, value)
|
||||||
|
|
||||||
@ -115,4 +135,28 @@ def setup_membership():
|
|||||||
else:
|
else:
|
||||||
plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm")
|
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"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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_job_card_details
|
||||||
erpnext.patches.v13_0.update_level_in_bom #1234sswef
|
erpnext.patches.v13_0.update_level_in_bom #1234sswef
|
||||||
erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry
|
erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry
|
||||||
|
erpnext.patches.v13_0.update_subscription_status_in_memberships
|
||||||
|
@ -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…
x
Reference in New Issue
Block a user