Skip to content
Open

Prod #19

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
77b8fca
feat: Introduce `ensure_mode_of_payment_exists` for GrayQuest payment…
badal8381 Jan 30, 2026
79ae11f
status field fix in student applicant
wal-arjun1801 Feb 9, 2026
2b35c44
Merge pull request #14 from UnityAppSuite/statusfieldfix
badal8381 Feb 9, 2026
ab07b29
feat: Introduce `resolve_payment_request` utility to robustly identif…
badal8381 Feb 9, 2026
9645929
feat: Sanitize name fields for GrayQuest API compatibility and enhanc…
badal8381 Feb 9, 2026
6df0bfe
feat: Add 'Fees' doctype to direct total/current field assignment logic.
badal8381 Feb 10, 2026
6e9bbc7
Merge pull request #15 from UnityAppSuite/ga-webhook
badal8381 Feb 10, 2026
fdfb746
feat: Redact sensitive headers in GrayQuest API request logs, add dat…
badal8381 Feb 10, 2026
a1b9609
feat: store bank_reference_id and UTR in Payment Request reference_no
SamarthPansare Feb 11, 2026
889dc6e
feat: Add logic to resolve Payment Request and Student from EMI webho…
badal8381 Feb 11, 2026
4e983ba
fix: only assign student gender if value is MALE or FEMALE.
badal8381 Feb 11, 2026
abf720e
feat: apply mobile number cleaning and strip whitespace from email fi…
badal8381 Feb 12, 2026
c634d84
feat: enhance webhook log document and student resolution with multip…
badal8381 Feb 13, 2026
7be7915
fix: adjust late fee on EMI disbursement with term-wise support
SamarthPansare Feb 16, 2026
8d2b9b6
Merge pull request #16 from UnityAppSuite/fix/emi-late-fee-adjustment
badal8381 Feb 17, 2026
8271f2f
fix: use webhook date as Payment Entry posting_date instead of today
SamarthPansare Mar 13, 2026
9cb3430
Merge pull request #20 from UnityAppSuite/fix/webhook-posting-date
badal8381 Mar 17, 2026
04beafe
fix: use correct Ticket field names in Grayquest fee header calculati…
Awarth Jun 16, 2026
36bc5cc
Fix ticket student_details to send dict instead of list
Awarth Jun 23, 2026
233ea93
Merge pull request #23 from UnityAppSuite/feature/gq-ticket-student-d…
badal8381 Jun 23, 2026
7fb0219
fix: accumulate fee header amounts in _get_ticket_fee_headers function
Awarth Jun 29, 2026
918a6f1
Merge pull request #24 from UnityAppSuite/feature/gq-ticket-student-d…
badal8381 Jun 29, 2026
d5e996a
feat: redirect fee payments to walsh SPA payment-status page for deep…
Awarth Jul 29, 2026
5e87e40
Merge pull request #25 from UnityAppSuite/feat/fee-payment-deep-link
Awarth Jul 30, 2026
9458496
fix: include application details in payment validation process
Awarth Aug 21, 2026
0d336ee
Merge pull request #28 from UnityAppSuite/event-quick-fix
Awarth Aug 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions grayquest/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,24 @@ def handle_payment_callback(**kwargs):
frappe.local.response["location"] = "/"
return

# Build return URL
# Build return URLs
payment_hash = frappe.db.get_value("Payment Request", payment_request, "payment_hash")
return_url = f"/payment?payment_request={payment_hash}" if payment_hash else "/"
base_url = frappe.utils.get_url()
if payment_hash:
success_url = f"{base_url}/tgaa-connect/payment-status?status=success&payment_request={payment_hash}"
failure_url = f"{base_url}/tgaa-connect/payment-status?status=failure&payment_request={payment_hash}"
else:
success_url = failure_url = "/"

# Skip if already paid
if frappe.db.get_value("Payment Request", payment_request, "status") == "Paid":
frappe.local.response["type"] = "redirect"
frappe.local.response["location"] = return_url
frappe.local.response["location"] = success_url
return

# Security: Require application_code to prevent URL tampering
# GrayQuest always sends application_code on successful payment
payment_succeeded = False
if status == "success" and application_code:
frappe.db.set_value("Payment Request", payment_request, "transaction_id", application_code)
doc = frappe.get_doc("Payment Request", payment_request)
Expand All @@ -64,9 +70,10 @@ def handle_payment_callback(**kwargs):
doc.on_payment_authorized(status="Completed")

frappe.db.commit()
payment_succeeded = True

frappe.local.response["type"] = "redirect"
frappe.local.response["location"] = return_url
frappe.local.response["location"] = success_url if payment_succeeded else failure_url

except Exception as e:
frappe.log_error(title="GrayQuest Callback Error", message=frappe.get_traceback())
Expand Down
157 changes: 121 additions & 36 deletions grayquest/grayquest/doctype/grayquest_settings/grayquest_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import frappe
import requests
from frappe import _, db, response
from frappe.integrations.utils import create_request_log
from frappe.model.document import Document
from frappe.utils import call_hook_method
from payments.utils import create_payment_gateway
Expand Down Expand Up @@ -41,51 +42,129 @@ def get_payment_url(self, **kwargs):
frappe.logger("grayquest").exception(frappe.get_traceback())
return url

def generate_url(self,kwargs):
headers = self.get_headers()
def generate_url(self, kwargs):
payload = get_payload(self, kwargs)
api_url = self.api_url.strip("/")
slug = self.slug
if kwargs.get("event", False):
slug = self.event_slug
endpoint = f"{api_url}/v1/pp/redirect/{slug}"

response = requests.post(endpoint, headers=headers, json=payload)
reference_docname = kwargs.get("reference_docname")
response = self.make_request(
method="POST",
endpoint=endpoint,
payload=payload,
reference_doctype=kwargs.get("reference_doctype"),
reference_docname=reference_docname,
request_description="GrayQuest Payment Request url",
)

if response.status_code == 201:
return response.json().get("data", {}).get("redirection_url")
else:
frappe.log_error(_("GrayQuest Payment Gateway Error"), response.json())
return response.json().get("message")
response_data = response.json()
frappe.log_error(_("GrayQuest Payment Gateway Error"), response_data)
frappe.throw(
_("GrayQuest Payment Error: {0}").format(response_data.get("message", "Unknown error")),
title=_("Payment Gateway Error"),
)

def get_headers(self):
if self.api_key and self.client_id and self.client_secret:
client_secret = self.get_password("client_secret")
api_key = self.get_password("api_key")
if not (self.api_key and self.client_id and self.client_secret):
frappe.throw(_("GrayQuest API Key, Client ID, and Client Secret are required"))

client_secret = self.get_password("client_secret")
api_key = self.get_password("api_key")

# Encode client_id and client_secret in base64
credentials = f"{self.client_id}:{client_secret}"
auth_token = base64.b64encode(credentials.encode()).decode()

return {
"Authorization": f"Basic {auth_token}",
"GQ-API-Key": api_key,
"Content-Type": "application/json",
}

def log_request(self,service_name, data, url=None, **kwargs):
"""Create an Integration Request log entry for GrayQuest API calls."""
return create_request_log(
data=data,
service_name=service_name,
url=url,
**kwargs,
)

def make_request(self, method, endpoint, payload=None, params=None,
reference_doctype=None, reference_docname=None,
request_description=None):
"""
Make an HTTP request to GrayQuest API and log it via Integration Request.

Args:
method: HTTP method ("GET" or "POST")
endpoint: Full API endpoint URL
payload: JSON body for POST requests
params: Query parameters for GET requests
reference_doctype: Linked document type for logging
reference_docname: Linked document name for logging
request_description: Description for the Integration Request log

Returns:
requests.Response: Response from the API
"""
headers = self.get_headers()

# Encode client_id and client_secret in base64
credentials = f"{self.client_id}:{client_secret}"
auth_token = base64.b64encode(credentials.encode()).decode()
redacted_headers = {
key: "******" if key in ("Authorization", "GQ-API-Key") else value
for key, value in headers.items()
}

integration_request = self.log_request(
service_name="GrayQuest",
data=payload or params or {},
url=endpoint,
request_headers=redacted_headers,
reference_doctype=reference_doctype,
reference_docname=reference_docname,
request_description=request_description,
)

# Headers
return {
"Authorization": f"Basic {auth_token}",
"GQ-API-Key": api_key,
"Content-Type": "application/json",
}
try:
if method == "POST":
response = requests.post(endpoint, headers=headers, json=payload)
elif method == "GET":
response = requests.get(endpoint, headers=headers, params=params)
else:
frappe.throw(_("Unsupported HTTP method: {0}").format(method))

response_data = response.json()

if response.ok:
integration_request.handle_success(response_data)
else:
integration_request.handle_failure(response_data)
frappe.db.commit()
return response
except Exception:
integration_request.handle_failure({"error": frappe.get_traceback()})
raise

def handle_webhook(self, data):
# Add webhook log
self.add_webhook_log(data)
if data.get("entity") == "direct":
return handle_payment_gateway_webhook(data)
elif data.get("entity") == "monthly-emi":
return handle_emi_webhook(data)
else:
frappe.log_error(_("Invalid Webhook Entity"))
return {
"status": "error",
"message": _("Invalid Webhook Entity"),
}

try:
if data.get("entity") == "direct":
result = handle_payment_gateway_webhook(data)
elif data.get("entity") == "monthly-emi":
result = handle_emi_webhook(data)
else:
frappe.log_error(_("Invalid Webhook Entity"))
return {"status": "error", "message": _("Invalid Webhook Entity")}
return result
except Exception:
raise

def add_webhook_log(self, data):
add_webhook_log(data)
Expand All @@ -97,13 +176,19 @@ def check_payment_status(self, payment_request):
"""
api_url = self.api_url.strip("/")
endpoint = f"{api_url}/v1/payments/fetch"
headers = self.get_headers()
transaction_id = db.get_value("Payment Request", payment_request, "transaction_id")
payload = {"application_code": transaction_id}
res = requests.get(endpoint, headers=headers, params=payload)

if res.status_code == 200:
response["message"] = res.json()
else:
params = {"application_code": transaction_id}

res = self.make_request(
method="GET",
endpoint=endpoint,
params=params,
reference_doctype="Payment Request",
reference_docname=payment_request,
request_description="GrayQuest Payment Status Check",
)

if res.status_code != 200:
frappe.log_error(_("GrayQuest Payment Gateway Error"), res.json())
response["message"] = res.json()

response["message"] = res.json()
Loading