Co-authored-by: Frappe Bot <developers@frappe.io>pull/2/head
@@ -2,25 +2,154 @@ | |||
# Copyright (c) 2018, Frappe Technologies and contributors | |||
# License: MIT. See LICENSE | |||
import frappe | |||
from frappe.model.document import Document | |||
from urllib.parse import urlencode | |||
import braintree | |||
import frappe | |||
from frappe import _ | |||
from urllib.parse import urlencode | |||
from frappe.utils import get_url, call_hook_method | |||
from frappe.integrations.utils import create_request_log, create_payment_gateway | |||
from frappe.integrations.utils import create_payment_gateway, create_request_log | |||
from frappe.model.document import Document | |||
from frappe.utils import call_hook_method, get_url | |||
class BraintreeSettings(Document): | |||
supported_currencies = [ | |||
"AED","AMD","AOA","ARS","AUD","AWG","AZN","BAM","BBD","BDT","BGN","BIF","BMD","BND","BOB", | |||
"BRL","BSD","BWP","BYN","BZD","CAD","CHF","CLP","CNY","COP","CRC","CVE","CZK","DJF","DKK", | |||
"DOP","DZD","EGP","ETB","EUR","FJD","FKP","GBP","GEL","GHS","GIP","GMD","GNF","GTQ","GYD", | |||
"HKD","HNL","HRK","HTG","HUF","IDR","ILS","INR","ISK","JMD","JPY","KES","KGS","KHR","KMF", | |||
"KRW","KYD","KZT","LAK","LBP","LKR","LRD","LSL","LTL","MAD","MDL","MKD","MNT","MOP","MUR", | |||
"MVR","MWK","MXN","MYR","MZN","NAD","NGN","NIO","NOK","NPR","NZD","PAB","PEN","PGK","PHP", | |||
"PKR","PLN","PYG","QAR","RON","RSD","RUB","RWF","SAR","SBD","SCR","SEK","SGD","SHP","SLL", | |||
"SOS","SRD","STD","SVC","SYP","SZL","THB","TJS","TOP","TRY","TTD","TWD","TZS","UAH","UGX", | |||
"USD","UYU","UZS","VEF","VND","VUV","WST","XAF","XCD","XOF","XPF","YER","ZAR","ZMK","ZWD" | |||
"AED", | |||
"AMD", | |||
"AOA", | |||
"ARS", | |||
"AUD", | |||
"AWG", | |||
"AZN", | |||
"BAM", | |||
"BBD", | |||
"BDT", | |||
"BGN", | |||
"BIF", | |||
"BMD", | |||
"BND", | |||
"BOB", | |||
"BRL", | |||
"BSD", | |||
"BWP", | |||
"BYN", | |||
"BZD", | |||
"CAD", | |||
"CHF", | |||
"CLP", | |||
"CNY", | |||
"COP", | |||
"CRC", | |||
"CVE", | |||
"CZK", | |||
"DJF", | |||
"DKK", | |||
"DOP", | |||
"DZD", | |||
"EGP", | |||
"ETB", | |||
"EUR", | |||
"FJD", | |||
"FKP", | |||
"GBP", | |||
"GEL", | |||
"GHS", | |||
"GIP", | |||
"GMD", | |||
"GNF", | |||
"GTQ", | |||
"GYD", | |||
"HKD", | |||
"HNL", | |||
"HRK", | |||
"HTG", | |||
"HUF", | |||
"IDR", | |||
"ILS", | |||
"INR", | |||
"ISK", | |||
"JMD", | |||
"JPY", | |||
"KES", | |||
"KGS", | |||
"KHR", | |||
"KMF", | |||
"KRW", | |||
"KYD", | |||
"KZT", | |||
"LAK", | |||
"LBP", | |||
"LKR", | |||
"LRD", | |||
"LSL", | |||
"LTL", | |||
"MAD", | |||
"MDL", | |||
"MKD", | |||
"MNT", | |||
"MOP", | |||
"MUR", | |||
"MVR", | |||
"MWK", | |||
"MXN", | |||
"MYR", | |||
"MZN", | |||
"NAD", | |||
"NGN", | |||
"NIO", | |||
"NOK", | |||
"NPR", | |||
"NZD", | |||
"PAB", | |||
"PEN", | |||
"PGK", | |||
"PHP", | |||
"PKR", | |||
"PLN", | |||
"PYG", | |||
"QAR", | |||
"RON", | |||
"RSD", | |||
"RUB", | |||
"RWF", | |||
"SAR", | |||
"SBD", | |||
"SCR", | |||
"SEK", | |||
"SGD", | |||
"SHP", | |||
"SLL", | |||
"SOS", | |||
"SRD", | |||
"STD", | |||
"SVC", | |||
"SYP", | |||
"SZL", | |||
"THB", | |||
"TJS", | |||
"TOP", | |||
"TRY", | |||
"TTD", | |||
"TWD", | |||
"TZS", | |||
"UAH", | |||
"UGX", | |||
"USD", | |||
"UYU", | |||
"UZS", | |||
"VEF", | |||
"VND", | |||
"VUV", | |||
"WST", | |||
"XAF", | |||
"XCD", | |||
"XOF", | |||
"XPF", | |||
"YER", | |||
"ZAR", | |||
"ZMK", | |||
"ZWD", | |||
] | |||
def validate(self): | |||
@@ -28,25 +157,31 @@ class BraintreeSettings(Document): | |||
self.configure_braintree() | |||
def on_update(self): | |||
create_payment_gateway('Braintree-' + self.gateway_name, settings='Braintree Settings', controller=self.gateway_name) | |||
call_hook_method('payment_gateway_enabled', gateway='Braintree-' + self.gateway_name) | |||
create_payment_gateway( | |||
"Braintree-" + self.gateway_name, settings="Braintree Settings", controller=self.gateway_name | |||
) | |||
call_hook_method("payment_gateway_enabled", gateway="Braintree-" + self.gateway_name) | |||
def configure_braintree(self): | |||
if self.use_sandbox: | |||
environment = 'sandbox' | |||
environment = "sandbox" | |||
else: | |||
environment = 'production' | |||
environment = "production" | |||
braintree.Configuration.configure( | |||
environment=environment, | |||
merchant_id=self.merchant_id, | |||
public_key=self.public_key, | |||
private_key=self.get_password(fieldname='private_key',raise_exception=False) | |||
private_key=self.get_password(fieldname="private_key", raise_exception=False), | |||
) | |||
def validate_transaction_currency(self, currency): | |||
if currency not in self.supported_currencies: | |||
frappe.throw(_("Please select another payment method. Stripe does not support transactions in currency '{0}'").format(currency)) | |||
frappe.throw( | |||
_( | |||
"Please select another payment method. Stripe does not support transactions in currency '{0}'" | |||
).format(currency) | |||
) | |||
def get_payment_url(self, **kwargs): | |||
return get_url("./integrations/braintree_checkout?{0}".format(urlencode(kwargs))) | |||
@@ -60,48 +195,62 @@ class BraintreeSettings(Document): | |||
except Exception: | |||
frappe.log_error(frappe.get_traceback()) | |||
return{ | |||
"redirect_to": frappe.redirect_to_message(_('Server Error'), _("There seems to be an issue with the server's braintree configuration. Don't worry, in case of failure, the amount will get refunded to your account.")), | |||
"status": 401 | |||
return { | |||
"redirect_to": frappe.redirect_to_message( | |||
_("Server Error"), | |||
_( | |||
"There seems to be an issue with the server's braintree configuration. Don't worry, in case of failure, the amount will get refunded to your account." | |||
), | |||
), | |||
"status": 401, | |||
} | |||
def create_charge_on_braintree(self): | |||
self.configure_braintree() | |||
redirect_to = self.data.get('redirect_to') or None | |||
redirect_message = self.data.get('redirect_message') or None | |||
redirect_to = self.data.get("redirect_to") or None | |||
redirect_message = self.data.get("redirect_message") or None | |||
result = braintree.Transaction.sale({ | |||
"amount": self.data.amount, | |||
"payment_method_nonce": self.data.payload_nonce, | |||
"options": { | |||
"submit_for_settlement": True | |||
result = braintree.Transaction.sale( | |||
{ | |||
"amount": self.data.amount, | |||
"payment_method_nonce": self.data.payload_nonce, | |||
"options": {"submit_for_settlement": True}, | |||
} | |||
}) | |||
) | |||
if result.is_success: | |||
self.integration_request.db_set('status', 'Completed', update_modified=False) | |||
self.integration_request.db_set("status", "Completed", update_modified=False) | |||
self.flags.status_changed_to = "Completed" | |||
self.integration_request.db_set('output', result.transaction.status, update_modified=False) | |||
self.integration_request.db_set("output", result.transaction.status, update_modified=False) | |||
elif result.transaction: | |||
self.integration_request.db_set('status', 'Failed', update_modified=False) | |||
error_log = frappe.log_error("code: " + str(result.transaction.processor_response_code) + " | text: " + str(result.transaction.processor_response_text), "Braintree Payment Error") | |||
self.integration_request.db_set('error', error_log.error, update_modified=False) | |||
self.integration_request.db_set("status", "Failed", update_modified=False) | |||
error_log = frappe.log_error( | |||
"code: " | |||
+ str(result.transaction.processor_response_code) | |||
+ " | text: " | |||
+ str(result.transaction.processor_response_text), | |||
"Braintree Payment Error", | |||
) | |||
self.integration_request.db_set("error", error_log.error, update_modified=False) | |||
else: | |||
self.integration_request.db_set('status', 'Failed', update_modified=False) | |||
self.integration_request.db_set("status", "Failed", update_modified=False) | |||
for error in result.errors.deep_errors: | |||
error_log = frappe.log_error("code: " + str(error.code) + " | message: " + str(error.message), "Braintree Payment Error") | |||
self.integration_request.db_set('error', error_log.error, update_modified=False) | |||
error_log = frappe.log_error( | |||
"code: " + str(error.code) + " | message: " + str(error.message), "Braintree Payment Error" | |||
) | |||
self.integration_request.db_set("error", error_log.error, update_modified=False) | |||
if self.flags.status_changed_to == "Completed": | |||
status = 'Completed' | |||
status = "Completed" | |||
if self.data.reference_doctype and self.data.reference_docname: | |||
custom_redirect_to = None | |||
try: | |||
custom_redirect_to = frappe.get_doc(self.data.reference_doctype, | |||
self.data.reference_docname).run_method("on_payment_authorized", self.flags.status_changed_to) | |||
braintree_success_page = frappe.get_hooks('braintree_success_page') | |||
custom_redirect_to = frappe.get_doc( | |||
self.data.reference_doctype, self.data.reference_docname | |||
).run_method("on_payment_authorized", self.flags.status_changed_to) | |||
braintree_success_page = frappe.get_hooks("braintree_success_page") | |||
if braintree_success_page: | |||
custom_redirect_to = frappe.get_attr(braintree_success_page[-1])(self.data) | |||
except Exception: | |||
@@ -110,26 +259,27 @@ class BraintreeSettings(Document): | |||
if custom_redirect_to: | |||
redirect_to = custom_redirect_to | |||
redirect_url = 'payment-success' | |||
redirect_url = "payment-success" | |||
else: | |||
status = 'Error' | |||
redirect_url = 'payment-failed' | |||
status = "Error" | |||
redirect_url = "payment-failed" | |||
if redirect_to: | |||
redirect_url += '?' + urlencode({'redirect_to': redirect_to}) | |||
redirect_url += "?" + urlencode({"redirect_to": redirect_to}) | |||
if redirect_message: | |||
redirect_url += '&' + urlencode({'redirect_message': redirect_message}) | |||
redirect_url += "&" + urlencode({"redirect_message": redirect_message}) | |||
return {"redirect_to": redirect_url, "status": status} | |||
return { | |||
"redirect_to": redirect_url, | |||
"status": status | |||
} | |||
def get_gateway_controller(doc): | |||
payment_request = frappe.get_doc("Payment Request", doc) | |||
gateway_controller = frappe.db.get_value("Payment Gateway", payment_request.payment_gateway, "gateway_controller") | |||
gateway_controller = frappe.db.get_value( | |||
"Payment Gateway", payment_request.payment_gateway, "gateway_controller" | |||
) | |||
return gateway_controller | |||
def get_client_token(doc): | |||
gateway_controller = get_gateway_controller(doc) | |||
settings = frappe.get_doc("Braintree Settings", gateway_controller) | |||
@@ -3,5 +3,6 @@ | |||
# License: MIT. See LICENSE | |||
import unittest | |||
class TestBraintreeSettings(unittest.TestCase): | |||
pass |
@@ -63,21 +63,48 @@ More Details: | |||
""" | |||
import frappe | |||
import json | |||
from urllib.parse import urlencode | |||
import pytz | |||
import frappe | |||
from frappe import _ | |||
from urllib.parse import urlencode | |||
from frappe.integrations.utils import create_payment_gateway, create_request_log, make_post_request | |||
from frappe.model.document import Document | |||
from frappe.integrations.utils import create_request_log, make_post_request, create_payment_gateway | |||
from frappe.utils import get_url, call_hook_method, cint, get_datetime | |||
from frappe.utils import call_hook_method, cint, get_datetime, get_url | |||
api_path = "/api/method/frappe.integrations.doctype.paypal_settings.paypal_settings" | |||
api_path = '/api/method/frappe.integrations.doctype.paypal_settings.paypal_settings' | |||
class PayPalSettings(Document): | |||
supported_currencies = ["AUD", "BRL", "CAD", "CZK", "DKK", "EUR", "HKD", "HUF", "ILS", "JPY", "MYR", "MXN", | |||
"TWD", "NZD", "NOK", "PHP", "PLN", "GBP", "RUB", "SGD", "SEK", "CHF", "THB", "TRY", "USD"] | |||
supported_currencies = [ | |||
"AUD", | |||
"BRL", | |||
"CAD", | |||
"CZK", | |||
"DKK", | |||
"EUR", | |||
"HKD", | |||
"HUF", | |||
"ILS", | |||
"JPY", | |||
"MYR", | |||
"MXN", | |||
"TWD", | |||
"NZD", | |||
"NOK", | |||
"PHP", | |||
"PLN", | |||
"GBP", | |||
"RUB", | |||
"SGD", | |||
"SEK", | |||
"CHF", | |||
"THB", | |||
"TRY", | |||
"USD", | |||
] | |||
def __setup__(self): | |||
setattr(self, "use_sandbox", 0) | |||
@@ -88,7 +115,7 @@ class PayPalSettings(Document): | |||
def validate(self): | |||
create_payment_gateway("PayPal") | |||
call_hook_method('payment_gateway_enabled', gateway="PayPal") | |||
call_hook_method("payment_gateway_enabled", gateway="PayPal") | |||
if not self.flags.ignore_mandatory: | |||
self.validate_paypal_credentails() | |||
@@ -97,7 +124,11 @@ class PayPalSettings(Document): | |||
def validate_transaction_currency(self, currency): | |||
if currency not in self.supported_currencies: | |||
frappe.throw(_("Please select another payment method. PayPal does not support transactions in currency '{0}'").format(currency)) | |||
frappe.throw( | |||
_( | |||
"Please select another payment method. PayPal does not support transactions in currency '{0}'" | |||
).format(currency) | |||
) | |||
def get_paypal_params_and_url(self): | |||
params = { | |||
@@ -105,17 +136,23 @@ class PayPalSettings(Document): | |||
"PWD": self.get_password(fieldname="api_password", raise_exception=False), | |||
"SIGNATURE": self.signature, | |||
"VERSION": "98", | |||
"METHOD": "GetPalDetails" | |||
"METHOD": "GetPalDetails", | |||
} | |||
if hasattr(self, "use_sandbox") and self.use_sandbox: | |||
params.update({ | |||
"USER": frappe.conf.sandbox_api_username, | |||
"PWD": frappe.conf.sandbox_api_password, | |||
"SIGNATURE": frappe.conf.sandbox_signature | |||
}) | |||
api_url = "https://api-3t.sandbox.paypal.com/nvp" if (self.paypal_sandbox or self.use_sandbox) else "https://api-3t.paypal.com/nvp" | |||
params.update( | |||
{ | |||
"USER": frappe.conf.sandbox_api_username, | |||
"PWD": frappe.conf.sandbox_api_password, | |||
"SIGNATURE": frappe.conf.sandbox_signature, | |||
} | |||
) | |||
api_url = ( | |||
"https://api-3t.sandbox.paypal.com/nvp" | |||
if (self.paypal_sandbox or self.use_sandbox) | |||
else "https://api-3t.paypal.com/nvp" | |||
) | |||
return params, api_url | |||
@@ -142,27 +179,30 @@ class PayPalSettings(Document): | |||
else: | |||
return_url = "https://www.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token={0}" | |||
kwargs.update({ | |||
"token": response.get("TOKEN")[0], | |||
"correlation_id": response.get("CORRELATIONID")[0] | |||
}) | |||
self.integration_request = create_request_log(kwargs, "Remote", "PayPal", response.get("TOKEN")[0]) | |||
kwargs.update( | |||
{"token": response.get("TOKEN")[0], "correlation_id": response.get("CORRELATIONID")[0]} | |||
) | |||
self.integration_request = create_request_log( | |||
kwargs, "Remote", "PayPal", response.get("TOKEN")[0] | |||
) | |||
return return_url.format(kwargs["token"]) | |||
def execute_set_express_checkout(self, **kwargs): | |||
params, url = self.get_paypal_params_and_url() | |||
params.update({ | |||
"METHOD": "SetExpressCheckout", | |||
"returnUrl": get_url("{0}.get_express_checkout_details".format(api_path)), | |||
"cancelUrl": get_url("/payment-cancel"), | |||
"PAYMENTREQUEST_0_PAYMENTACTION": "SALE", | |||
"PAYMENTREQUEST_0_AMT": kwargs['amount'], | |||
"PAYMENTREQUEST_0_CURRENCYCODE": kwargs['currency'].upper() | |||
}) | |||
if kwargs.get('subscription_details'): | |||
params.update( | |||
{ | |||
"METHOD": "SetExpressCheckout", | |||
"returnUrl": get_url("{0}.get_express_checkout_details".format(api_path)), | |||
"cancelUrl": get_url("/payment-cancel"), | |||
"PAYMENTREQUEST_0_PAYMENTACTION": "SALE", | |||
"PAYMENTREQUEST_0_AMT": kwargs["amount"], | |||
"PAYMENTREQUEST_0_CURRENCYCODE": kwargs["currency"].upper(), | |||
} | |||
) | |||
if kwargs.get("subscription_details"): | |||
self.configure_recurring_payments(params, kwargs) | |||
params = urlencode(params) | |||
@@ -175,14 +215,20 @@ class PayPalSettings(Document): | |||
def configure_recurring_payments(self, params, kwargs): | |||
# removing the params as we have to setup rucurring payments | |||
for param in ('PAYMENTREQUEST_0_PAYMENTACTION', 'PAYMENTREQUEST_0_AMT', | |||
'PAYMENTREQUEST_0_CURRENCYCODE'): | |||
for param in ( | |||
"PAYMENTREQUEST_0_PAYMENTACTION", | |||
"PAYMENTREQUEST_0_AMT", | |||
"PAYMENTREQUEST_0_CURRENCYCODE", | |||
): | |||
del params[param] | |||
params.update({ | |||
"L_BILLINGTYPE0": "RecurringPayments", #The type of billing agreement | |||
"L_BILLINGAGREEMENTDESCRIPTION0": kwargs['description'] | |||
}) | |||
params.update( | |||
{ | |||
"L_BILLINGTYPE0": "RecurringPayments", # The type of billing agreement | |||
"L_BILLINGAGREEMENTDESCRIPTION0": kwargs["description"], | |||
} | |||
) | |||
def get_paypal_and_transaction_details(token): | |||
doc = frappe.get_doc("PayPal Settings") | |||
@@ -194,23 +240,25 @@ def get_paypal_and_transaction_details(token): | |||
return data, params, url | |||
def setup_redirect(data, redirect_url, custom_redirect_to=None, redirect=True): | |||
redirect_to = data.get('redirect_to') or None | |||
redirect_message = data.get('redirect_message') or None | |||
redirect_to = data.get("redirect_to") or None | |||
redirect_message = data.get("redirect_message") or None | |||
if custom_redirect_to: | |||
redirect_to = custom_redirect_to | |||
if redirect_to: | |||
redirect_url += '&' + urlencode({'redirect_to': redirect_to}) | |||
redirect_url += "&" + urlencode({"redirect_to": redirect_to}) | |||
if redirect_message: | |||
redirect_url += '&' + urlencode({'redirect_message': redirect_message}) | |||
redirect_url += "&" + urlencode({"redirect_message": redirect_message}) | |||
# this is done so that functions called via hooks can update flags.redirect_to | |||
if redirect: | |||
frappe.local.response["type"] = "redirect" | |||
frappe.local.response["location"] = get_url(redirect_url) | |||
@frappe.whitelist(allow_guest=True, xss_safe=True) | |||
def get_express_checkout_details(token): | |||
try: | |||
@@ -218,26 +266,29 @@ def get_express_checkout_details(token): | |||
doc.setup_sandbox_env(token) | |||
params, url = doc.get_paypal_params_and_url() | |||
params.update({ | |||
"METHOD": "GetExpressCheckoutDetails", | |||
"TOKEN": token | |||
}) | |||
params.update({"METHOD": "GetExpressCheckoutDetails", "TOKEN": token}) | |||
response = make_post_request(url, data=params) | |||
if response.get("ACK")[0] != "Success": | |||
frappe.respond_as_web_page(_("Something went wrong"), | |||
_("Looks like something went wrong during the transaction. Since we haven't confirmed the payment, Paypal will automatically refund you this amount. If it doesn't, please send us an email and mention the Correlation ID: {0}.").format(response.get("CORRELATIONID", [None])[0]), | |||
indicator_color='red', | |||
http_status_code=frappe.ValidationError.http_status_code) | |||
frappe.respond_as_web_page( | |||
_("Something went wrong"), | |||
_( | |||
"Looks like something went wrong during the transaction. Since we haven't confirmed the payment, Paypal will automatically refund you this amount. If it doesn't, please send us an email and mention the Correlation ID: {0}." | |||
).format(response.get("CORRELATIONID", [None])[0]), | |||
indicator_color="red", | |||
http_status_code=frappe.ValidationError.http_status_code, | |||
) | |||
return | |||
doc = frappe.get_doc("Integration Request", token) | |||
update_integration_request_status(token, { | |||
"payerid": response.get("PAYERID")[0], | |||
"payer_email": response.get("EMAIL")[0] | |||
}, "Authorized", doc=doc) | |||
update_integration_request_status( | |||
token, | |||
{"payerid": response.get("PAYERID")[0], "payer_email": response.get("EMAIL")[0]}, | |||
"Authorized", | |||
doc=doc, | |||
) | |||
frappe.local.response["type"] = "redirect" | |||
frappe.local.response["location"] = get_redirect_uri(doc, token, response.get("PAYERID")[0]) | |||
@@ -245,35 +296,45 @@ def get_express_checkout_details(token): | |||
except Exception: | |||
frappe.log_error(frappe.get_traceback()) | |||
@frappe.whitelist(allow_guest=True, xss_safe=True) | |||
def confirm_payment(token): | |||
try: | |||
custom_redirect_to = None | |||
data, params, url = get_paypal_and_transaction_details(token) | |||
params.update({ | |||
"METHOD": "DoExpressCheckoutPayment", | |||
"PAYERID": data.get("payerid"), | |||
"TOKEN": token, | |||
"PAYMENTREQUEST_0_PAYMENTACTION": "SALE", | |||
"PAYMENTREQUEST_0_AMT": data.get("amount"), | |||
"PAYMENTREQUEST_0_CURRENCYCODE": data.get("currency").upper() | |||
}) | |||
params.update( | |||
{ | |||
"METHOD": "DoExpressCheckoutPayment", | |||
"PAYERID": data.get("payerid"), | |||
"TOKEN": token, | |||
"PAYMENTREQUEST_0_PAYMENTACTION": "SALE", | |||
"PAYMENTREQUEST_0_AMT": data.get("amount"), | |||
"PAYMENTREQUEST_0_CURRENCYCODE": data.get("currency").upper(), | |||
} | |||
) | |||
response = make_post_request(url, data=params) | |||
if response.get("ACK")[0] == "Success": | |||
update_integration_request_status(token, { | |||
"transaction_id": response.get("PAYMENTINFO_0_TRANSACTIONID")[0], | |||
"correlation_id": response.get("CORRELATIONID")[0] | |||
}, "Completed") | |||
update_integration_request_status( | |||
token, | |||
{ | |||
"transaction_id": response.get("PAYMENTINFO_0_TRANSACTIONID")[0], | |||
"correlation_id": response.get("CORRELATIONID")[0], | |||
}, | |||
"Completed", | |||
) | |||
if data.get("reference_doctype") and data.get("reference_docname"): | |||
custom_redirect_to = frappe.get_doc(data.get("reference_doctype"), | |||
data.get("reference_docname")).run_method("on_payment_authorized", "Completed") | |||
custom_redirect_to = frappe.get_doc( | |||
data.get("reference_doctype"), data.get("reference_docname") | |||
).run_method("on_payment_authorized", "Completed") | |||
frappe.db.commit() | |||
redirect_url = '/integrations/payment-success?doctype={0}&docname={1}'.format(data.get("reference_doctype"), data.get("reference_docname")) | |||
redirect_url = "/integrations/payment-success?doctype={0}&docname={1}".format( | |||
data.get("reference_doctype"), data.get("reference_docname") | |||
) | |||
else: | |||
redirect_url = "/integrations/payment-failed" | |||
@@ -282,6 +343,7 @@ def confirm_payment(token): | |||
except Exception: | |||
frappe.log_error(frappe.get_traceback()) | |||
@frappe.whitelist(allow_guest=True, xss_safe=True) | |||
def create_recurring_profile(token, payerid): | |||
try: | |||
@@ -292,49 +354,60 @@ def create_recurring_profile(token, payerid): | |||
addons = data.get("addons") | |||
subscription_details = data.get("subscription_details") | |||
if data.get('subscription_id'): | |||
if data.get("subscription_id"): | |||
if addons: | |||
updating = True | |||
manage_recurring_payment_profile_status(data['subscription_id'], 'Cancel', params, url) | |||
params.update({ | |||
"METHOD": "CreateRecurringPaymentsProfile", | |||
"PAYERID": payerid, | |||
"TOKEN": token, | |||
"DESC": data.get("description"), | |||
"BILLINGPERIOD": subscription_details.get("billing_period"), | |||
"BILLINGFREQUENCY": subscription_details.get("billing_frequency"), | |||
"AMT": data.get("amount") if data.get("subscription_amount") == data.get("amount") else data.get("subscription_amount"), | |||
"CURRENCYCODE": data.get("currency").upper(), | |||
"INITAMT": data.get("upfront_amount") | |||
}) | |||
status_changed_to = 'Completed' if data.get("starting_immediately") or updating else 'Verified' | |||
manage_recurring_payment_profile_status(data["subscription_id"], "Cancel", params, url) | |||
params.update( | |||
{ | |||
"METHOD": "CreateRecurringPaymentsProfile", | |||
"PAYERID": payerid, | |||
"TOKEN": token, | |||
"DESC": data.get("description"), | |||
"BILLINGPERIOD": subscription_details.get("billing_period"), | |||
"BILLINGFREQUENCY": subscription_details.get("billing_frequency"), | |||
"AMT": data.get("amount") | |||
if data.get("subscription_amount") == data.get("amount") | |||
else data.get("subscription_amount"), | |||
"CURRENCYCODE": data.get("currency").upper(), | |||
"INITAMT": data.get("upfront_amount"), | |||
} | |||
) | |||
status_changed_to = "Completed" if data.get("starting_immediately") or updating else "Verified" | |||
starts_at = get_datetime(subscription_details.get("start_date")) or frappe.utils.now_datetime() | |||
starts_at = starts_at.replace(tzinfo=pytz.timezone(frappe.utils.get_time_zone())).astimezone(pytz.utc) | |||
starts_at = starts_at.replace(tzinfo=pytz.timezone(frappe.utils.get_time_zone())).astimezone( | |||
pytz.utc | |||
) | |||
#"PROFILESTARTDATE": datetime.utcfromtimestamp(get_timestamp(starts_at)).isoformat() | |||
params.update({ | |||
"PROFILESTARTDATE": starts_at.isoformat() | |||
}) | |||
# "PROFILESTARTDATE": datetime.utcfromtimestamp(get_timestamp(starts_at)).isoformat() | |||
params.update({"PROFILESTARTDATE": starts_at.isoformat()}) | |||
response = make_post_request(url, data=params) | |||
if response.get("ACK")[0] == "Success": | |||
update_integration_request_status(token, { | |||
"profile_id": response.get("PROFILEID")[0], | |||
}, "Completed") | |||
update_integration_request_status( | |||
token, | |||
{ | |||
"profile_id": response.get("PROFILEID")[0], | |||
}, | |||
"Completed", | |||
) | |||
if data.get("reference_doctype") and data.get("reference_docname"): | |||
data['subscription_id'] = response.get("PROFILEID")[0] | |||
data["subscription_id"] = response.get("PROFILEID")[0] | |||
frappe.flags.data = data | |||
custom_redirect_to = frappe.get_doc(data.get("reference_doctype"), | |||
data.get("reference_docname")).run_method("on_payment_authorized", status_changed_to) | |||
custom_redirect_to = frappe.get_doc( | |||
data.get("reference_doctype"), data.get("reference_docname") | |||
).run_method("on_payment_authorized", status_changed_to) | |||
frappe.db.commit() | |||
redirect_url = '/integrations/payment-success?doctype={0}&docname={1}'.format(data.get("reference_doctype"), data.get("reference_docname")) | |||
redirect_url = "/integrations/payment-success?doctype={0}&docname={1}".format( | |||
data.get("reference_doctype"), data.get("reference_docname") | |||
) | |||
else: | |||
redirect_url = "/integrations/payment-failed" | |||
@@ -343,26 +416,29 @@ def create_recurring_profile(token, payerid): | |||
except Exception: | |||
frappe.log_error(frappe.get_traceback()) | |||
def update_integration_request_status(token, data, status, error=False, doc=None): | |||
if not doc: | |||
doc = frappe.get_doc("Integration Request", token) | |||
doc.update_status(data, status) | |||
def get_redirect_uri(doc, token, payerid): | |||
data = json.loads(doc.data) | |||
if data.get("subscription_details") or data.get("subscription_id"): | |||
return get_url("{0}.create_recurring_profile?token={1}&payerid={2}".format(api_path, token, payerid)) | |||
return get_url( | |||
"{0}.create_recurring_profile?token={1}&payerid={2}".format(api_path, token, payerid) | |||
) | |||
else: | |||
return get_url("{0}.confirm_payment?token={1}".format(api_path, token)) | |||
def manage_recurring_payment_profile_status(profile_id, action, args, url): | |||
args.update({ | |||
"METHOD": "ManageRecurringPaymentsProfileStatus", | |||
"PROFILEID": profile_id, | |||
"ACTION": action | |||
}) | |||
args.update( | |||
{"METHOD": "ManageRecurringPaymentsProfileStatus", "PROFILEID": profile_id, "ACTION": action} | |||
) | |||
response = make_post_request(url, data=args) | |||
@@ -370,9 +446,10 @@ def manage_recurring_payment_profile_status(profile_id, action, args, url): | |||
# thus could not cancel the subscription. | |||
# thus raise an exception only if the error code is not equal to 11556 | |||
if response.get("ACK")[0] != "Success" and response.get("L_ERRORCODE0", [])[0] != '11556': | |||
if response.get("ACK")[0] != "Success" and response.get("L_ERRORCODE0", [])[0] != "11556": | |||
frappe.throw(_("Failed while amending subscription")) | |||
@frappe.whitelist(allow_guest=True) | |||
def ipn_handler(): | |||
try: | |||
@@ -380,26 +457,32 @@ def ipn_handler(): | |||
validate_ipn_request(data) | |||
data.update({ | |||
"payment_gateway": "PayPal" | |||
}) | |||
data.update({"payment_gateway": "PayPal"}) | |||
doc = frappe.get_doc({ | |||
"data": json.dumps(frappe.local.form_dict), | |||
"doctype": "Integration Request", | |||
"integration_type": "Subscription Notification", | |||
"status": "Queued" | |||
}).insert(ignore_permissions=True) | |||
doc = frappe.get_doc( | |||
{ | |||
"data": json.dumps(frappe.local.form_dict), | |||
"doctype": "Integration Request", | |||
"integration_type": "Subscription Notification", | |||
"status": "Queued", | |||
} | |||
).insert(ignore_permissions=True) | |||
frappe.db.commit() | |||
frappe.enqueue(method='frappe.integrations.doctype.paypal_settings.paypal_settings.handle_subscription_notification', | |||
queue='long', timeout=600, is_async=True, **{"doctype": "Integration Request", "docname": doc.name}) | |||
frappe.enqueue( | |||
method="frappe.integrations.doctype.paypal_settings.paypal_settings.handle_subscription_notification", | |||
queue="long", | |||
timeout=600, | |||
is_async=True, | |||
**{"doctype": "Integration Request", "docname": doc.name} | |||
) | |||
except frappe.InvalidStatusError: | |||
pass | |||
except Exception as e: | |||
frappe.log(frappe.log_error(title=e)) | |||
def validate_ipn_request(data): | |||
def _throw(): | |||
frappe.throw(_("In Valid Request"), exc=frappe.InvalidStatusError) | |||
@@ -410,16 +493,16 @@ def validate_ipn_request(data): | |||
doc = frappe.get_doc("PayPal Settings") | |||
params, url = doc.get_paypal_params_and_url() | |||
params.update({ | |||
"METHOD": "GetRecurringPaymentsProfileDetails", | |||
"PROFILEID": data.get("recurring_payment_id") | |||
}) | |||
params.update( | |||
{"METHOD": "GetRecurringPaymentsProfileDetails", "PROFILEID": data.get("recurring_payment_id")} | |||
) | |||
params = urlencode(params) | |||
res = make_post_request(url=url, data=params.encode("utf-8")) | |||
if res['ACK'][0] != 'Success': | |||
if res["ACK"][0] != "Success": | |||
_throw() | |||
def handle_subscription_notification(doctype, docname): | |||
call_hook_method("handle_subscription_notification", doctype=doctype, docname=docname) |
@@ -3,112 +3,131 @@ | |||
# License: MIT. See LICENSE | |||
import json | |||
import requests | |||
from urllib.parse import urlencode | |||
import requests | |||
from paytmchecksum import generateSignature, verifySignature | |||
import frappe | |||
from frappe.model.document import Document | |||
from frappe import _ | |||
from frappe.utils import get_url, call_hook_method, cint, flt, cstr | |||
from frappe.integrations.utils import create_request_log, create_payment_gateway | |||
from frappe.utils import get_request_site_address | |||
from paytmchecksum import generateSignature, verifySignature | |||
from frappe.integrations.utils import create_payment_gateway, create_request_log | |||
from frappe.model.document import Document | |||
from frappe.utils import call_hook_method, cint, cstr, flt, get_request_site_address, get_url | |||
from frappe.utils.password import get_decrypted_password | |||
class PaytmSettings(Document): | |||
supported_currencies = ["INR"] | |||
def validate(self): | |||
create_payment_gateway('Paytm') | |||
call_hook_method('payment_gateway_enabled', gateway='Paytm') | |||
create_payment_gateway("Paytm") | |||
call_hook_method("payment_gateway_enabled", gateway="Paytm") | |||
def validate_transaction_currency(self, currency): | |||
if currency not in self.supported_currencies: | |||
frappe.throw(_("Please select another payment method. Paytm does not support transactions in currency '{0}'").format(currency)) | |||
frappe.throw( | |||
_( | |||
"Please select another payment method. Paytm does not support transactions in currency '{0}'" | |||
).format(currency) | |||
) | |||
def get_payment_url(self, **kwargs): | |||
'''Return payment url with several params''' | |||
"""Return payment url with several params""" | |||
# create unique order id by making it equal to the integration request | |||
integration_request = create_request_log(kwargs, "Host", "Paytm") | |||
kwargs.update(dict(order_id=integration_request.name)) | |||
return get_url("./integrations/paytm_checkout?{0}".format(urlencode(kwargs))) | |||
def get_paytm_config(): | |||
''' Returns paytm config ''' | |||
"""Returns paytm config""" | |||
paytm_config = frappe.db.get_singles_dict('Paytm Settings') | |||
paytm_config.update(dict(merchant_key=get_decrypted_password('Paytm Settings', 'Paytm Settings', 'merchant_key'))) | |||
paytm_config = frappe.db.get_singles_dict("Paytm Settings") | |||
paytm_config.update( | |||
dict(merchant_key=get_decrypted_password("Paytm Settings", "Paytm Settings", "merchant_key")) | |||
) | |||
if cint(paytm_config.staging): | |||
paytm_config.update(dict( | |||
website="WEBSTAGING", | |||
url='https://securegw-stage.paytm.in/order/process', | |||
transaction_status_url='https://securegw-stage.paytm.in/order/status', | |||
industry_type_id='RETAIL' | |||
)) | |||
paytm_config.update( | |||
dict( | |||
website="WEBSTAGING", | |||
url="https://securegw-stage.paytm.in/order/process", | |||
transaction_status_url="https://securegw-stage.paytm.in/order/status", | |||
industry_type_id="RETAIL", | |||
) | |||
) | |||
else: | |||
paytm_config.update(dict( | |||
url='https://securegw.paytm.in/order/process', | |||
transaction_status_url='https://securegw.paytm.in/order/status', | |||
)) | |||
paytm_config.update( | |||
dict( | |||
url="https://securegw.paytm.in/order/process", | |||
transaction_status_url="https://securegw.paytm.in/order/status", | |||
) | |||
) | |||
return paytm_config | |||
def get_paytm_params(payment_details, order_id, paytm_config): | |||
# initialize a dictionary | |||
paytm_params = dict() | |||
redirect_uri = get_request_site_address(True) + "/api/method/frappe.integrations.doctype.paytm_settings.paytm_settings.verify_transaction" | |||
redirect_uri = ( | |||
get_request_site_address(True) | |||
+ "/api/method/frappe.integrations.doctype.paytm_settings.paytm_settings.verify_transaction" | |||
) | |||
paytm_params.update({ | |||
"MID" : paytm_config.merchant_id, | |||
"WEBSITE" : paytm_config.website, | |||
"INDUSTRY_TYPE_ID" : paytm_config.industry_type_id, | |||
"CHANNEL_ID" : "WEB", | |||
"ORDER_ID" : order_id, | |||
"CUST_ID" : payment_details['payer_email'], | |||
"EMAIL" : payment_details['payer_email'], | |||
"TXN_AMOUNT" : cstr(flt(payment_details['amount'], 2)), | |||
"CALLBACK_URL" : redirect_uri, | |||
}) | |||
paytm_params.update( | |||
{ | |||
"MID": paytm_config.merchant_id, | |||
"WEBSITE": paytm_config.website, | |||
"INDUSTRY_TYPE_ID": paytm_config.industry_type_id, | |||
"CHANNEL_ID": "WEB", | |||
"ORDER_ID": order_id, | |||
"CUST_ID": payment_details["payer_email"], | |||
"EMAIL": payment_details["payer_email"], | |||
"TXN_AMOUNT": cstr(flt(payment_details["amount"], 2)), | |||
"CALLBACK_URL": redirect_uri, | |||
} | |||
) | |||
checksum = generateSignature(paytm_params, paytm_config.merchant_key) | |||
paytm_params.update({ | |||
"CHECKSUMHASH" : checksum | |||
}) | |||
paytm_params.update({"CHECKSUMHASH": checksum}) | |||
return paytm_params | |||
@frappe.whitelist(allow_guest=True) | |||
def verify_transaction(**paytm_params): | |||
'''Verify checksum for received data in the callback and then verify the transaction''' | |||
"""Verify checksum for received data in the callback and then verify the transaction""" | |||
paytm_config = get_paytm_config() | |||
is_valid_checksum = False | |||
paytm_params.pop('cmd', None) | |||
paytm_checksum = paytm_params.pop('CHECKSUMHASH', None) | |||
paytm_params.pop("cmd", None) | |||
paytm_checksum = paytm_params.pop("CHECKSUMHASH", None) | |||
if paytm_params and paytm_config and paytm_checksum: | |||
# Verify checksum | |||
is_valid_checksum = verifySignature(paytm_params, paytm_config.merchant_key, paytm_checksum) | |||
if is_valid_checksum and paytm_params.get('RESPCODE') == '01': | |||
verify_transaction_status(paytm_config, paytm_params['ORDERID']) | |||
if is_valid_checksum and paytm_params.get("RESPCODE") == "01": | |||
verify_transaction_status(paytm_config, paytm_params["ORDERID"]) | |||
else: | |||
frappe.respond_as_web_page("Payment Failed", | |||
frappe.respond_as_web_page( | |||
"Payment Failed", | |||
"Transaction failed to complete. In case of any deductions, deducted amount will get refunded to your account.", | |||
http_status_code=401, indicator_color='red') | |||
frappe.log_error("Order unsuccessful. Failed Response:"+cstr(paytm_params), 'Paytm Payment Failed') | |||
http_status_code=401, | |||
indicator_color="red", | |||
) | |||
frappe.log_error( | |||
"Order unsuccessful. Failed Response:" + cstr(paytm_params), "Paytm Payment Failed" | |||
) | |||
def verify_transaction_status(paytm_config, order_id): | |||
'''Verify transaction completion after checksum has been verified''' | |||
paytm_params=dict( | |||
MID=paytm_config.merchant_id, | |||
ORDERID= order_id | |||
) | |||
"""Verify transaction completion after checksum has been verified""" | |||
paytm_params = dict(MID=paytm_config.merchant_id, ORDERID=order_id) | |||
checksum = generateSignature(paytm_params, paytm_config.merchant_key) | |||
paytm_params["CHECKSUMHASH"] = checksum | |||
@@ -116,43 +135,48 @@ def verify_transaction_status(paytm_config, order_id): | |||
post_data = json.dumps(paytm_params) | |||
url = paytm_config.transaction_status_url | |||
response = requests.post(url, data = post_data, headers = {"Content-type": "application/json"}).json() | |||
response = requests.post(url, data=post_data, headers={"Content-type": "application/json"}).json() | |||
finalize_request(order_id, response) | |||
def finalize_request(order_id, transaction_response): | |||
request = frappe.get_doc('Integration Request', order_id) | |||
request = frappe.get_doc("Integration Request", order_id) | |||
transaction_data = frappe._dict(json.loads(request.data)) | |||
redirect_to = transaction_data.get('redirect_to') or None | |||
redirect_message = transaction_data.get('redirect_message') or None | |||
redirect_to = transaction_data.get("redirect_to") or None | |||
redirect_message = transaction_data.get("redirect_message") or None | |||
if transaction_response['STATUS'] == "TXN_SUCCESS": | |||
if transaction_response["STATUS"] == "TXN_SUCCESS": | |||
if transaction_data.reference_doctype and transaction_data.reference_docname: | |||
custom_redirect_to = None | |||
try: | |||
custom_redirect_to = frappe.get_doc(transaction_data.reference_doctype, | |||
transaction_data.reference_docname).run_method("on_payment_authorized", 'Completed') | |||
request.db_set('status', 'Completed') | |||
custom_redirect_to = frappe.get_doc( | |||
transaction_data.reference_doctype, transaction_data.reference_docname | |||
).run_method("on_payment_authorized", "Completed") | |||
request.db_set("status", "Completed") | |||
except Exception: | |||
request.db_set('status', 'Failed') | |||
request.db_set("status", "Failed") | |||
frappe.log_error(frappe.get_traceback()) | |||
if custom_redirect_to: | |||
redirect_to = custom_redirect_to | |||
redirect_url = '/integrations/payment-success' | |||
redirect_url = "/integrations/payment-success" | |||
else: | |||
request.db_set('status', 'Failed') | |||
redirect_url = '/integrations/payment-failed' | |||
request.db_set("status", "Failed") | |||
redirect_url = "/integrations/payment-failed" | |||
if redirect_to: | |||
redirect_url += '?' + urlencode({'redirect_to': redirect_to}) | |||
redirect_url += "?" + urlencode({"redirect_to": redirect_to}) | |||
if redirect_message: | |||
redirect_url += '&' + urlencode({'redirect_message': redirect_message}) | |||
redirect_url += "&" + urlencode({"redirect_message": redirect_message}) | |||
frappe.local.response["type"] = "redirect" | |||
frappe.local.response["location"] = redirect_url | |||
frappe.local.response['type'] = 'redirect' | |||
frappe.local.response['location'] = redirect_url | |||
def get_gateway_controller(doctype, docname): | |||
reference_doc = frappe.get_doc(doctype, docname) | |||
gateway_controller = frappe.db.get_value("Payment Gateway", reference_doc.payment_gateway, "gateway_controller") | |||
return gateway_controller | |||
gateway_controller = frappe.db.get_value( | |||
"Payment Gateway", reference_doc.payment_gateway, "gateway_controller" | |||
) | |||
return gateway_controller |
@@ -4,5 +4,6 @@ | |||
# import frappe | |||
import unittest | |||
class TestPaytmSettings(unittest.TestCase): | |||
pass |
@@ -60,17 +60,24 @@ For razorpay payment status is Authorized | |||
""" | |||
import frappe | |||
from frappe import _ | |||
import json | |||
import hmac | |||
import razorpay | |||
import hashlib | |||
import hmac | |||
import json | |||
from urllib.parse import urlencode | |||
import razorpay | |||
import frappe | |||
from frappe import _ | |||
from frappe.integrations.utils import ( | |||
create_payment_gateway, | |||
create_request_log, | |||
make_get_request, | |||
make_post_request, | |||
) | |||
from frappe.model.document import Document | |||
from frappe.utils import get_url, call_hook_method, cint, get_timestamp | |||
from frappe.integrations.utils import (make_get_request, make_post_request, create_request_log, | |||
create_payment_gateway) | |||
from frappe.utils import call_hook_method, cint, get_timestamp, get_url | |||
class RazorpaySettings(Document): | |||
supported_currencies = ["INR"] | |||
@@ -81,37 +88,45 @@ class RazorpaySettings(Document): | |||
self.client = razorpay.Client(auth=(self.api_key, secret)) | |||
def validate(self): | |||
create_payment_gateway('Razorpay') | |||
call_hook_method('payment_gateway_enabled', gateway='Razorpay') | |||
create_payment_gateway("Razorpay") | |||
call_hook_method("payment_gateway_enabled", gateway="Razorpay") | |||
if not self.flags.ignore_mandatory: | |||
self.validate_razorpay_credentails() | |||
def validate_razorpay_credentails(self): | |||
if self.api_key and self.api_secret: | |||
try: | |||
make_get_request(url="https://api.razorpay.com/v1/payments", | |||
auth=(self.api_key, self.get_password(fieldname="api_secret", raise_exception=False))) | |||
make_get_request( | |||
url="https://api.razorpay.com/v1/payments", | |||
auth=(self.api_key, self.get_password(fieldname="api_secret", raise_exception=False)), | |||
) | |||
except Exception: | |||
frappe.throw(_("Seems API Key or API Secret is wrong !!!")) | |||
def validate_transaction_currency(self, currency): | |||
if currency not in self.supported_currencies: | |||
frappe.throw(_("Please select another payment method. Razorpay does not support transactions in currency '{0}'").format(currency)) | |||
frappe.throw( | |||
_( | |||
"Please select another payment method. Razorpay does not support transactions in currency '{0}'" | |||
).format(currency) | |||
) | |||
def setup_addon(self, settings, **kwargs): | |||
""" | |||
Addon template: | |||
{ | |||
"item": { | |||
"name": row.upgrade_type, | |||
"amount": row.amount, | |||
"currency": currency, | |||
"description": "add-on description" | |||
}, | |||
"quantity": 1 (The total amount is calculated as item.amount * quantity) | |||
} | |||
Addon template: | |||
{ | |||
"item": { | |||
"name": row.upgrade_type, | |||
"amount": row.amount, | |||
"currency": currency, | |||
"description": "add-on description" | |||
}, | |||
"quantity": 1 (The total amount is calculated as item.amount * quantity) | |||
} | |||
""" | |||
url = "https://api.razorpay.com/v1/subscriptions/{0}/addons".format(kwargs.get('subscription_id')) | |||
url = "https://api.razorpay.com/v1/subscriptions/{0}/addons".format( | |||
kwargs.get("subscription_id") | |||
) | |||
try: | |||
if not frappe.conf.converted_rupee_to_paisa: | |||
@@ -122,52 +137,49 @@ class RazorpaySettings(Document): | |||
url, | |||
auth=(settings.api_key, settings.api_secret), | |||
data=json.dumps(addon), | |||
headers={ | |||
"content-type": "application/json" | |||
} | |||
headers={"content-type": "application/json"}, | |||
) | |||
if not resp.get('id'): | |||
frappe.log_error(str(resp), 'Razorpay Failed while creating subscription') | |||
if not resp.get("id"): | |||
frappe.log_error(str(resp), "Razorpay Failed while creating subscription") | |||
except: | |||
frappe.log_error(frappe.get_traceback()) | |||
# failed | |||
pass | |||
def setup_subscription(self, settings, **kwargs): | |||
start_date = get_timestamp(kwargs.get('subscription_details').get("start_date")) \ | |||
if kwargs.get('subscription_details').get("start_date") else None | |||
start_date = ( | |||
get_timestamp(kwargs.get("subscription_details").get("start_date")) | |||
if kwargs.get("subscription_details").get("start_date") | |||
else None | |||
) | |||
subscription_details = { | |||
"plan_id": kwargs.get('subscription_details').get("plan_id"), | |||
"total_count": kwargs.get('subscription_details').get("billing_frequency"), | |||
"customer_notify": kwargs.get('subscription_details').get("customer_notify") | |||
"plan_id": kwargs.get("subscription_details").get("plan_id"), | |||
"total_count": kwargs.get("subscription_details").get("billing_frequency"), | |||
"customer_notify": kwargs.get("subscription_details").get("customer_notify"), | |||
} | |||
if start_date: | |||
subscription_details['start_at'] = cint(start_date) | |||
subscription_details["start_at"] = cint(start_date) | |||
if kwargs.get('addons'): | |||
if kwargs.get("addons"): | |||
convert_rupee_to_paisa(**kwargs) | |||
subscription_details.update({ | |||
"addons": kwargs.get('addons') | |||
}) | |||
subscription_details.update({"addons": kwargs.get("addons")}) | |||
try: | |||
resp = make_post_request( | |||
"https://api.razorpay.com/v1/subscriptions", | |||
auth=(settings.api_key, settings.api_secret), | |||
data=json.dumps(subscription_details), | |||
headers={ | |||
"content-type": "application/json" | |||
} | |||
headers={"content-type": "application/json"}, | |||
) | |||
if resp.get('status') == 'created': | |||
kwargs['subscription_id'] = resp.get('id') | |||
frappe.flags.status = 'created' | |||
if resp.get("status") == "created": | |||
kwargs["subscription_id"] = resp.get("id") | |||
frappe.flags.status = "created" | |||
return kwargs | |||
else: | |||
frappe.log_error(str(resp), 'Razorpay Failed while creating subscription') | |||
frappe.log_error(str(resp), "Razorpay Failed while creating subscription") | |||
except: | |||
frappe.log_error(frappe.get_traceback()) | |||
@@ -178,8 +190,8 @@ class RazorpaySettings(Document): | |||
if not kwargs.get("subscription_id"): | |||
kwargs = self.setup_subscription(settings, **kwargs) | |||
if frappe.flags.status !='created': | |||
kwargs['subscription_id'] = None | |||
if frappe.flags.status != "created": | |||
kwargs["subscription_id"] = None | |||
return kwargs | |||
@@ -191,25 +203,27 @@ class RazorpaySettings(Document): | |||
# Creating Orders https://razorpay.com/docs/api/orders/ | |||
# convert rupees to paisa | |||
kwargs['amount'] *= 100 | |||
kwargs["amount"] *= 100 | |||
# Create integration log | |||
integration_request = create_request_log(kwargs, "Host", "Razorpay") | |||
# Setup payment options | |||
payment_options = { | |||
"amount": kwargs.get('amount'), | |||
"currency": kwargs.get('currency', 'INR'), | |||
"receipt": kwargs.get('receipt'), | |||
"payment_capture": kwargs.get('payment_capture') | |||
"amount": kwargs.get("amount"), | |||
"currency": kwargs.get("currency", "INR"), | |||
"receipt": kwargs.get("receipt"), | |||
"payment_capture": kwargs.get("payment_capture"), | |||
} | |||
if self.api_key and self.api_secret: | |||
try: | |||
order = make_post_request("https://api.razorpay.com/v1/orders", | |||
order = make_post_request( | |||
"https://api.razorpay.com/v1/orders", | |||
auth=(self.api_key, self.get_password(fieldname="api_secret", raise_exception=False)), | |||
data=payment_options) | |||
order['integration_request'] = integration_request.name | |||
return order # Order returned to be consumed by razorpay.js | |||
data=payment_options, | |||
) | |||
order["integration_request"] = integration_request.name | |||
return order # Order returned to be consumed by razorpay.js | |||
except Exception: | |||
frappe.log(frappe.get_traceback()) | |||
frappe.throw(_("Could not create razorpay order")) | |||
@@ -219,14 +233,19 @@ class RazorpaySettings(Document): | |||
try: | |||
self.integration_request = frappe.get_doc("Integration Request", self.data.token) | |||
self.integration_request.update_status(self.data, 'Queued') | |||
self.integration_request.update_status(self.data, "Queued") | |||
return self.authorize_payment() | |||
except Exception: | |||
frappe.log_error(frappe.get_traceback()) | |||
return{ | |||
"redirect_to": frappe.redirect_to_message(_('Server Error'), _("Seems issue with server's razorpay config. Don't worry, in case of failure amount will get refunded to your account.")), | |||
"status": 401 | |||
return { | |||
"redirect_to": frappe.redirect_to_message( | |||
_("Server Error"), | |||
_( | |||
"Seems issue with server's razorpay config. Don't worry, in case of failure amount will get refunded to your account." | |||
), | |||
), | |||
"status": 401, | |||
} | |||
def authorize_payment(self): | |||
@@ -239,29 +258,30 @@ class RazorpaySettings(Document): | |||
settings = self.get_settings(data) | |||
try: | |||
resp = make_get_request("https://api.razorpay.com/v1/payments/{0}" | |||
.format(self.data.razorpay_payment_id), auth=(settings.api_key, | |||
settings.api_secret)) | |||
resp = make_get_request( | |||
"https://api.razorpay.com/v1/payments/{0}".format(self.data.razorpay_payment_id), | |||
auth=(settings.api_key, settings.api_secret), | |||
) | |||
if resp.get("status") == "authorized": | |||
self.integration_request.update_status(data, 'Authorized') | |||
self.integration_request.update_status(data, "Authorized") | |||
self.flags.status_changed_to = "Authorized" | |||
if resp.get("status") == "captured": | |||
self.integration_request.update_status(data, 'Completed') | |||
self.integration_request.update_status(data, "Completed") | |||
self.flags.status_changed_to = "Completed" | |||
elif data.get('subscription_id'): | |||
elif data.get("subscription_id"): | |||
if resp.get("status") == "refunded": | |||
# if subscription start date is in future then | |||
# razorpay refunds the amount after authorizing the card details | |||
# thus changing status to Verified | |||
self.integration_request.update_status(data, 'Completed') | |||
self.integration_request.update_status(data, "Completed") | |||
self.flags.status_changed_to = "Verified" | |||
else: | |||
frappe.log_error(str(resp), 'Razorpay Payment not authorized') | |||
frappe.log_error(str(resp), "Razorpay Payment not authorized") | |||
except: | |||
frappe.log_error(frappe.get_traceback()) | |||
@@ -270,15 +290,16 @@ class RazorpaySettings(Document): | |||
status = frappe.flags.integration_request.status_code | |||
redirect_to = data.get('redirect_to') or None | |||
redirect_message = data.get('redirect_message') or None | |||
redirect_to = data.get("redirect_to") or None | |||
redirect_message = data.get("redirect_message") or None | |||
if self.flags.status_changed_to in ("Authorized", "Verified", "Completed"): | |||
if self.data.reference_doctype and self.data.reference_docname: | |||
custom_redirect_to = None | |||
try: | |||
frappe.flags.data = data | |||
custom_redirect_to = frappe.get_doc(self.data.reference_doctype, | |||
self.data.reference_docname).run_method("on_payment_authorized", self.flags.status_changed_to) | |||
custom_redirect_to = frappe.get_doc( | |||
self.data.reference_doctype, self.data.reference_docname | |||
).run_method("on_payment_authorized", self.flags.status_changed_to) | |||
except Exception: | |||
frappe.log_error(frappe.get_traceback()) | |||
@@ -286,31 +307,34 @@ class RazorpaySettings(Document): | |||
if custom_redirect_to: | |||
redirect_to = custom_redirect_to | |||
redirect_url = 'payment-success?doctype={0}&docname={1}'.format(self.data.reference_doctype, self.data.reference_docname) | |||
redirect_url = "payment-success?doctype={0}&docname={1}".format( | |||
self.data.reference_doctype, self.data.reference_docname | |||
) | |||
else: | |||
redirect_url = 'payment-failed' | |||
redirect_url = "payment-failed" | |||
if redirect_to: | |||
redirect_url += '&' + urlencode({'redirect_to': redirect_to}) | |||
redirect_url += "&" + urlencode({"redirect_to": redirect_to}) | |||
if redirect_message: | |||
redirect_url += '&' + urlencode({'redirect_message': redirect_message}) | |||
redirect_url += "&" + urlencode({"redirect_message": redirect_message}) | |||
return { | |||
"redirect_to": redirect_url, | |||
"status": status | |||
} | |||
return {"redirect_to": redirect_url, "status": status} | |||
def get_settings(self, data): | |||
settings = frappe._dict({ | |||
"api_key": self.api_key, | |||
"api_secret": self.get_password(fieldname="api_secret", raise_exception=False) | |||
}) | |||
settings = frappe._dict( | |||
{ | |||
"api_key": self.api_key, | |||
"api_secret": self.get_password(fieldname="api_secret", raise_exception=False), | |||
} | |||
) | |||
if cint(data.get('notes', {}).get('use_sandbox')) or data.get("use_sandbox"): | |||
settings.update({ | |||
"api_key": frappe.conf.sandbox_api_key, | |||
"api_secret": frappe.conf.sandbox_api_secret, | |||
}) | |||
if cint(data.get("notes", {}).get("use_sandbox")) or data.get("use_sandbox"): | |||
settings.update( | |||
{ | |||
"api_key": frappe.conf.sandbox_api_key, | |||
"api_secret": frappe.conf.sandbox_api_secret, | |||
} | |||
) | |||
return settings | |||
@@ -318,15 +342,16 @@ class RazorpaySettings(Document): | |||
settings = self.get_settings({}) | |||
try: | |||
resp = make_post_request("https://api.razorpay.com/v1/subscriptions/{0}/cancel" | |||
.format(subscription_id), auth=(settings.api_key, | |||
settings.api_secret)) | |||
resp = make_post_request( | |||
"https://api.razorpay.com/v1/subscriptions/{0}/cancel".format(subscription_id), | |||
auth=(settings.api_key, settings.api_secret), | |||
) | |||
except Exception: | |||
frappe.log_error(frappe.get_traceback()) | |||
def verify_signature(self, body, signature, key): | |||
key = bytes(key, 'utf-8') | |||
body = bytes(body, 'utf-8') | |||
key = bytes(key, "utf-8") | |||
body = bytes(body, "utf-8") | |||
dig = hmac.new(key=key, msg=body, digestmod=hashlib.sha256) | |||
@@ -334,22 +359,26 @@ class RazorpaySettings(Document): | |||
result = hmac.compare_digest(generated_signature, signature) | |||
if not result: | |||
frappe.throw(_('Razorpay Signature Verification Failed'), exc=frappe.PermissionError) | |||
frappe.throw(_("Razorpay Signature Verification Failed"), exc=frappe.PermissionError) | |||
return result | |||
def capture_payment(is_sandbox=False, sanbox_response=None): | |||
""" | |||
Verifies the purchase as complete by the merchant. | |||
After capture, the amount is transferred to the merchant within T+3 days | |||
where T is the day on which payment is captured. | |||
Verifies the purchase as complete by the merchant. | |||
After capture, the amount is transferred to the merchant within T+3 days | |||
where T is the day on which payment is captured. | |||
Note: Attempting to capture a payment whose status is not authorized will produce an error. | |||
Note: Attempting to capture a payment whose status is not authorized will produce an error. | |||
""" | |||
controller = frappe.get_doc("Razorpay Settings") | |||
for doc in frappe.get_all("Integration Request", filters={"status": "Authorized", | |||
"integration_request_service": "Razorpay"}, fields=["name", "data"]): | |||
for doc in frappe.get_all( | |||
"Integration Request", | |||
filters={"status": "Authorized", "integration_request_service": "Razorpay"}, | |||
fields=["name", "data"], | |||
): | |||
try: | |||
if is_sandbox: | |||
resp = sanbox_response | |||
@@ -357,12 +386,18 @@ def capture_payment(is_sandbox=False, sanbox_response=None): | |||
data = json.loads(doc.data) | |||
settings = controller.get_settings(data) | |||
resp = make_get_request("https://api.razorpay.com/v1/payments/{0}".format(data.get("razorpay_payment_id")), | |||
auth=(settings.api_key, settings.api_secret), data={"amount": data.get("amount")}) | |||
resp = make_get_request( | |||
"https://api.razorpay.com/v1/payments/{0}".format(data.get("razorpay_payment_id")), | |||
auth=(settings.api_key, settings.api_secret), | |||
data={"amount": data.get("amount")}, | |||
) | |||
if resp.get('status') == "authorized": | |||
resp = make_post_request("https://api.razorpay.com/v1/payments/{0}/capture".format(data.get("razorpay_payment_id")), | |||
auth=(settings.api_key, settings.api_secret), data={"amount": data.get("amount")}) | |||
if resp.get("status") == "authorized": | |||
resp = make_post_request( | |||
"https://api.razorpay.com/v1/payments/{0}/capture".format(data.get("razorpay_payment_id")), | |||
auth=(settings.api_key, settings.api_secret), | |||
data={"amount": data.get("amount")}, | |||
) | |||
if resp.get("status") == "captured": | |||
frappe.db.set_value("Integration Request", doc.name, "status", "Completed") | |||
@@ -372,7 +407,7 @@ def capture_payment(is_sandbox=False, sanbox_response=None): | |||
doc.status = "Failed" | |||
doc.error = frappe.get_traceback() | |||
doc.save() | |||
frappe.log_error(doc.error, '{0} Failed'.format(doc.name)) | |||
frappe.log_error(doc.error, "{0} Failed".format(doc.name)) | |||
@frappe.whitelist(allow_guest=True) | |||
@@ -380,6 +415,7 @@ def get_api_key(): | |||
controller = frappe.get_doc("Razorpay Settings") | |||
return controller.api_key | |||
@frappe.whitelist(allow_guest=True) | |||
def get_order(doctype, docname): | |||
# Order returned to be consumed by razorpay.js | |||
@@ -391,6 +427,7 @@ def get_order(doctype, docname): | |||
frappe.log_error(frappe.get_traceback(), _("Controller method get_razorpay_order missing")) | |||
frappe.throw(_("Could not create Razorpay order. Please contact Administrator")) | |||
@frappe.whitelist(allow_guest=True) | |||
def order_payment_success(integration_request, params): | |||
"""Called by razorpay.js on order payment success, the params | |||
@@ -398,8 +435,8 @@ def order_payment_success(integration_request, params): | |||
that is updated in the data field of integration request | |||
Args: | |||
integration_request (string): Name for integration request doc | |||
params (string): Params to be updated for integration request. | |||
integration_request (string): Name for integration request doc | |||
params (string): Params to be updated for integration request. | |||
""" | |||
params = json.loads(params) | |||
integration = frappe.get_doc("Integration Request", integration_request) | |||
@@ -418,25 +455,28 @@ def order_payment_success(integration_request, params): | |||
# Authorize payment | |||
controller.authorize_payment() | |||
@frappe.whitelist(allow_guest=True) | |||
def order_payment_failure(integration_request, params): | |||
"""Called by razorpay.js on failure | |||
Args: | |||
integration_request (TYPE): Description | |||
params (TYPE): error data to be updated | |||
integration_request (TYPE): Description | |||
params (TYPE): error data to be updated | |||
""" | |||
frappe.log_error(params, 'Razorpay Payment Failure') | |||
frappe.log_error(params, "Razorpay Payment Failure") | |||
params = json.loads(params) | |||
integration = frappe.get_doc("Integration Request", integration_request) | |||
integration.update_status(params, integration.status) | |||
def convert_rupee_to_paisa(**kwargs): | |||
for addon in kwargs.get('addons'): | |||
addon['item']['amount'] *= 100 | |||
for addon in kwargs.get("addons"): | |||
addon["item"]["amount"] *= 100 | |||
frappe.conf.converted_rupee_to_paisa = True | |||
@frappe.whitelist(allow_guest=True) | |||
def razorpay_subscription_callback(): | |||
try: | |||
@@ -444,44 +484,53 @@ def razorpay_subscription_callback(): | |||
validate_payment_callback(data) | |||
data.update({ | |||
"payment_gateway": "Razorpay" | |||
}) | |||
data.update({"payment_gateway": "Razorpay"}) | |||
doc = frappe.get_doc({ | |||
"data": json.dumps(frappe.local.form_dict), | |||
"doctype": "Integration Request", | |||
"integration_type": "Subscription Notification", | |||
"status": "Queued" | |||
}).insert(ignore_permissions=True) | |||
doc = frappe.get_doc( | |||
{ | |||
"data": json.dumps(frappe.local.form_dict), | |||
"doctype": "Integration Request", | |||
"integration_type": "Subscription Notification", | |||
"status": "Queued", | |||
} | |||
).insert(ignore_permissions=True) | |||
frappe.db.commit() | |||
frappe.enqueue(method='frappe.integrations.doctype.razorpay_settings.razorpay_settings.handle_subscription_notification', | |||
queue='long', timeout=600, is_async=True, **{"doctype": "Integration Request", "docname": doc.name}) | |||
frappe.enqueue( | |||
method="frappe.integrations.doctype.razorpay_settings.razorpay_settings.handle_subscription_notification", | |||
queue="long", | |||
timeout=600, | |||
is_async=True, | |||
**{"doctype": "Integration Request", "docname": doc.name} | |||
) | |||
except frappe.InvalidStatusError: | |||
pass | |||
except Exception as e: | |||
frappe.log(frappe.log_error(title=e)) | |||
def validate_payment_callback(data): | |||
def _throw(): | |||
frappe.throw(_("Invalid Subscription"), exc=frappe.InvalidStatusError) | |||
subscription_id = data.get('payload').get("subscription").get("entity").get("id") | |||
subscription_id = data.get("payload").get("subscription").get("entity").get("id") | |||
if not(subscription_id): | |||
if not (subscription_id): | |||
_throw() | |||
controller = frappe.get_doc("Razorpay Settings") | |||
settings = controller.get_settings(data) | |||
resp = make_get_request("https://api.razorpay.com/v1/subscriptions/{0}".format(subscription_id), | |||
auth=(settings.api_key, settings.api_secret)) | |||
resp = make_get_request( | |||
"https://api.razorpay.com/v1/subscriptions/{0}".format(subscription_id), | |||
auth=(settings.api_key, settings.api_secret), | |||
) | |||
if resp.get("status") != "active": | |||
_throw() | |||
def handle_subscription_notification(doctype, docname): | |||
call_hook_method("handle_subscription_notification", doctype=doctype, docname=docname) |
@@ -2,41 +2,171 @@ | |||
# Copyright (c) 2017, Frappe Technologies and contributors | |||
# License: MIT. See LICENSE | |||
from urllib.parse import urlencode | |||
import frappe | |||
from frappe.model.document import Document | |||
from frappe import _ | |||
from urllib.parse import urlencode | |||
from frappe.utils import get_url, call_hook_method, cint, flt | |||
from frappe.integrations.utils import make_get_request, make_post_request, create_request_log, create_payment_gateway | |||
from frappe.integrations.utils import ( | |||
create_payment_gateway, | |||
create_request_log, | |||
make_get_request, | |||
make_post_request, | |||
) | |||
from frappe.model.document import Document | |||
from frappe.utils import call_hook_method, cint, flt, get_url | |||
class StripeSettings(Document): | |||
supported_currencies = [ | |||
"AED", "ALL", "ANG", "ARS", "AUD", "AWG", "BBD", "BDT", "BIF", "BMD", "BND", | |||
"BOB", "BRL", "BSD", "BWP", "BZD", "CAD", "CHF", "CLP", "CNY", "COP", "CRC", "CVE", "CZK", "DJF", | |||
"DKK", "DOP", "DZD", "EGP", "ETB", "EUR", "FJD", "FKP", "GBP", "GIP", "GMD", "GNF", "GTQ", "GYD", | |||
"HKD", "HNL", "HRK", "HTG", "HUF", "IDR", "ILS", "INR", "ISK", "JMD", "JPY", "KES", "KHR", "KMF", | |||
"KRW", "KYD", "KZT", "LAK", "LBP", "LKR", "LRD", "MAD", "MDL", "MNT", "MOP", "MRO", "MUR", "MVR", | |||
"MWK", "MXN", "MYR", "NAD", "NGN", "NIO", "NOK", "NPR", "NZD", "PAB", "PEN", "PGK", "PHP", "PKR", | |||
"PLN", "PYG", "QAR", "RUB", "SAR", "SBD", "SCR", "SEK", "SGD", "SHP", "SLL", "SOS", "STD", "SVC", | |||
"SZL", "THB", "TOP", "TTD", "TWD", "TZS", "UAH", "UGX", "USD", "UYU", "UZS", "VND", "VUV", "WST", | |||
"XAF", "XOF", "XPF", "YER", "ZAR" | |||
"AED", | |||
"ALL", | |||
"ANG", | |||
"ARS", | |||
"AUD", | |||
"AWG", | |||
"BBD", | |||
"BDT", | |||
"BIF", | |||
"BMD", | |||
"BND", | |||
"BOB", | |||
"BRL", | |||
"BSD", | |||
"BWP", | |||
"BZD", | |||
"CAD", | |||
"CHF", | |||
"CLP", | |||
"CNY", | |||
"COP", | |||
"CRC", | |||
"CVE", | |||
"CZK", | |||
"DJF", | |||
"DKK", | |||
"DOP", | |||
"DZD", | |||
"EGP", | |||
"ETB", | |||
"EUR", | |||
"FJD", | |||
"FKP", | |||
"GBP", | |||
"GIP", | |||
"GMD", | |||
"GNF", | |||
"GTQ", | |||
"GYD", | |||
"HKD", | |||
"HNL", | |||
"HRK", | |||
"HTG", | |||
"HUF", | |||
"IDR", | |||
"ILS", | |||
"INR", | |||
"ISK", | |||
"JMD", | |||
"JPY", | |||
"KES", | |||
"KHR", | |||
"KMF", | |||
"KRW", | |||
"KYD", | |||
"KZT", | |||
"LAK", | |||
"LBP", | |||
"LKR", | |||
"LRD", | |||
"MAD", | |||
"MDL", | |||
"MNT", | |||
"MOP", | |||
"MRO", | |||
"MUR", | |||
"MVR", | |||
"MWK", | |||
"MXN", | |||
"MYR", | |||
"NAD", | |||
"NGN", | |||
"NIO", | |||
"NOK", | |||
"NPR", | |||
"NZD", | |||
"PAB", | |||
"PEN", | |||
"PGK", | |||
"PHP", | |||
"PKR", | |||
"PLN", | |||
"PYG", | |||
"QAR", | |||
"RUB", | |||
"SAR", | |||
"SBD", | |||
"SCR", | |||
"SEK", | |||
"SGD", | |||
"SHP", | |||
"SLL", | |||
"SOS", | |||
"STD", | |||
"SVC", | |||
"SZL", | |||
"THB", | |||
"TOP", | |||
"TTD", | |||
"TWD", | |||
"TZS", | |||
"UAH", | |||
"UGX", | |||
"USD", | |||
"UYU", | |||
"UZS", | |||
"VND", | |||
"VUV", | |||
"WST", | |||
"XAF", | |||
"XOF", | |||
"XPF", | |||
"YER", | |||
"ZAR", | |||
] | |||
currency_wise_minimum_charge_amount = { | |||
'JPY': 50, 'MXN': 10, 'DKK': 2.50, 'HKD': 4.00, 'NOK': 3.00, 'SEK': 3.00, | |||
'USD': 0.50, 'AUD': 0.50, 'BRL': 0.50, 'CAD': 0.50, 'CHF': 0.50, 'EUR': 0.50, | |||
'GBP': 0.30, 'NZD': 0.50, 'SGD': 0.50 | |||
"JPY": 50, | |||
"MXN": 10, | |||
"DKK": 2.50, | |||
"HKD": 4.00, | |||
"NOK": 3.00, | |||
"SEK": 3.00, | |||
"USD": 0.50, | |||
"AUD": 0.50, | |||
"BRL": 0.50, | |||
"CAD": 0.50, | |||
"CHF": 0.50, | |||
"EUR": 0.50, | |||
"GBP": 0.30, | |||
"NZD": 0.50, | |||
"SGD": 0.50, | |||
} | |||
def on_update(self): | |||
create_payment_gateway('Stripe-' + self.gateway_name, settings='Stripe Settings', controller=self.gateway_name) | |||
call_hook_method('payment_gateway_enabled', gateway='Stripe-' + self.gateway_name) | |||
create_payment_gateway( | |||
"Stripe-" + self.gateway_name, settings="Stripe Settings", controller=self.gateway_name | |||
) | |||
call_hook_method("payment_gateway_enabled", gateway="Stripe-" + self.gateway_name) | |||
if not self.flags.ignore_mandatory: | |||
self.validate_stripe_credentails() | |||
def validate_stripe_credentails(self): | |||
if self.publishable_key and self.secret_key: | |||
header = {"Authorization": "Bearer {0}".format(self.get_password(fieldname="secret_key", raise_exception=False))} | |||
header = { | |||
"Authorization": "Bearer {0}".format( | |||
self.get_password(fieldname="secret_key", raise_exception=False) | |||
) | |||
} | |||
try: | |||
make_get_request(url="https://api.stripe.com/v1/charges", headers=header) | |||
except Exception: | |||
@@ -44,19 +174,27 @@ class StripeSettings(Document): | |||
def validate_transaction_currency(self, currency): | |||
if currency not in self.supported_currencies: | |||
frappe.throw(_("Please select another payment method. Stripe does not support transactions in currency '{0}'").format(currency)) | |||
frappe.throw( | |||
_( | |||
"Please select another payment method. Stripe does not support transactions in currency '{0}'" | |||
).format(currency) | |||
) | |||
def validate_minimum_transaction_amount(self, currency, amount): | |||
if currency in self.currency_wise_minimum_charge_amount: | |||
if flt(amount) < self.currency_wise_minimum_charge_amount.get(currency, 0.0): | |||
frappe.throw(_("For currency {0}, the minimum transaction amount should be {1}").format(currency, | |||
self.currency_wise_minimum_charge_amount.get(currency, 0.0))) | |||
frappe.throw( | |||
_("For currency {0}, the minimum transaction amount should be {1}").format( | |||
currency, self.currency_wise_minimum_charge_amount.get(currency, 0.0) | |||
) | |||
) | |||
def get_payment_url(self, **kwargs): | |||
return get_url("./integrations/stripe_checkout?{0}".format(urlencode(kwargs))) | |||
def create_request(self, data): | |||
import stripe | |||
self.data = frappe._dict(data) | |||
stripe.api_key = self.get_password(fieldname="secret_key", raise_exception=False) | |||
stripe.default_http_client = stripe.http_client.RequestsClient() | |||
@@ -67,65 +205,77 @@ class StripeSettings(Document): | |||
except Exception: | |||
frappe.log_error(frappe.get_traceback()) | |||
return{ | |||
"redirect_to": frappe.redirect_to_message(_('Server Error'), _("It seems that there is an issue with the server's stripe configuration. In case of failure, the amount will get refunded to your account.")), | |||
"status": 401 | |||
return { | |||
"redirect_to": frappe.redirect_to_message( | |||
_("Server Error"), | |||
_( | |||
"It seems that there is an issue with the server's stripe configuration. In case of failure, the amount will get refunded to your account." | |||
), | |||
), | |||
"status": 401, | |||
} | |||
def create_charge_on_stripe(self): | |||
import stripe | |||
try: | |||
charge = stripe.Charge.create(amount=cint(flt(self.data.amount)*100), currency=self.data.currency, source=self.data.stripe_token_id, description=self.data.description, receipt_email=self.data.payer_email) | |||
charge = stripe.Charge.create( | |||
amount=cint(flt(self.data.amount) * 100), | |||
currency=self.data.currency, | |||
source=self.data.stripe_token_id, | |||
description=self.data.description, | |||
receipt_email=self.data.payer_email, | |||
) | |||
if charge.captured == True: | |||
self.integration_request.db_set('status', 'Completed', update_modified=False) | |||
self.integration_request.db_set("status", "Completed", update_modified=False) | |||
self.flags.status_changed_to = "Completed" | |||
else: | |||
frappe.log_error(charge.failure_message, 'Stripe Payment not completed') | |||
frappe.log_error(charge.failure_message, "Stripe Payment not completed") | |||
except Exception: | |||
frappe.log_error(frappe.get_traceback()) | |||
return self.finalize_request() | |||
def finalize_request(self): | |||
redirect_to = self.data.get('redirect_to') or None | |||
redirect_message = self.data.get('redirect_message') or None | |||
redirect_to = self.data.get("redirect_to") or None | |||
redirect_message = self.data.get("redirect_message") or None | |||
status = self.integration_request.status | |||
if self.flags.status_changed_to == "Completed": | |||
if self.data.reference_doctype and self.data.reference_docname: | |||
custom_redirect_to = None | |||
try: | |||
custom_redirect_to = frappe.get_doc(self.data.reference_doctype, | |||
self.data.reference_docname).run_method("on_payment_authorized", self.flags.status_changed_to) | |||
custom_redirect_to = frappe.get_doc( | |||
self.data.reference_doctype, self.data.reference_docname | |||
).run_method("on_payment_authorized", self.flags.status_changed_to) | |||
except Exception: | |||
frappe.log_error(frappe.get_traceback()) | |||
if custom_redirect_to: | |||
redirect_to = custom_redirect_to | |||
redirect_url = 'payment-success' | |||
redirect_url = "payment-success" | |||
if self.redirect_url: | |||
redirect_url = self.redirect_url | |||
redirect_to = None | |||
else: | |||
redirect_url = 'payment-failed' | |||
redirect_url = "payment-failed" | |||
if redirect_to: | |||
redirect_url += '?' + urlencode({'redirect_to': redirect_to}) | |||
redirect_url += "?" + urlencode({"redirect_to": redirect_to}) | |||
if redirect_message: | |||
redirect_url += '&' + urlencode({'redirect_message': redirect_message}) | |||
redirect_url += "&" + urlencode({"redirect_message": redirect_message}) | |||
return {"redirect_to": redirect_url, "status": status} | |||
return { | |||
"redirect_to": redirect_url, | |||
"status": status | |||
} | |||
def get_gateway_controller(doctype, docname): | |||
reference_doc = frappe.get_doc(doctype, docname) | |||
gateway_controller = frappe.db.get_value("Payment Gateway", reference_doc.payment_gateway, "gateway_controller") | |||
gateway_controller = frappe.db.get_value( | |||
"Payment Gateway", reference_doc.payment_gateway, "gateway_controller" | |||
) | |||
return gateway_controller |
@@ -3,5 +3,6 @@ | |||
# License: MIT. See LICENSE | |||
import unittest | |||
class TestStripeSettings(unittest.TestCase): | |||
pass |
@@ -5,5 +5,6 @@ | |||
import frappe | |||
from frappe.model.document import Document | |||
class PaymentGateway(Document): | |||
pass | |||
pass |
@@ -1,10 +1,12 @@ | |||
# -*- coding: utf-8 -*- | |||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
import frappe | |||
import unittest | |||
import frappe | |||
# test_records = frappe.get_test_records('Payment Gateway') | |||
class TestPaymentGateway(unittest.TestCase): | |||
pass |
@@ -1,16 +1,30 @@ | |||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
import json | |||
import frappe | |||
from frappe import _ | |||
from frappe.integrations.doctype.braintree_settings.braintree_settings import ( | |||
get_client_token, | |||
get_gateway_controller, | |||
) | |||
from frappe.utils import flt | |||
import json | |||
from frappe.integrations.doctype.braintree_settings.braintree_settings import get_client_token, get_gateway_controller | |||
no_cache = 1 | |||
expected_keys = ('amount', 'title', 'description', 'reference_doctype', 'reference_docname', | |||
'payer_name', 'payer_email', 'order_id', 'currency') | |||
expected_keys = ( | |||
"amount", | |||
"title", | |||
"description", | |||
"reference_doctype", | |||
"reference_docname", | |||
"payer_name", | |||
"payer_email", | |||
"order_id", | |||
"currency", | |||
) | |||
def get_context(context): | |||
context.no_cache = 1 | |||
@@ -22,26 +36,29 @@ def get_context(context): | |||
context.client_token = get_client_token(context.reference_docname) | |||
context['amount'] = flt(context['amount']) | |||
context["amount"] = flt(context["amount"]) | |||
gateway_controller = get_gateway_controller(context.reference_docname) | |||
context['header_img'] = frappe.db.get_value("Braintree Settings", gateway_controller, "header_img") | |||
context["header_img"] = frappe.db.get_value( | |||
"Braintree Settings", gateway_controller, "header_img" | |||
) | |||
else: | |||
frappe.redirect_to_message(_('Some information is missing'), | |||
_('Looks like someone sent you to an incomplete URL. Please ask them to look into it.')) | |||
frappe.redirect_to_message( | |||
_("Some information is missing"), | |||
_("Looks like someone sent you to an incomplete URL. Please ask them to look into it."), | |||
) | |||
frappe.local.flags.redirect_location = frappe.local.response.location | |||
raise frappe.Redirect | |||
@frappe.whitelist(allow_guest=True) | |||
def make_payment(payload_nonce, data, reference_doctype, reference_docname): | |||
data = json.loads(data) | |||
data.update({ | |||
"payload_nonce": payload_nonce | |||
}) | |||
data.update({"payload_nonce": payload_nonce}) | |||
gateway_controller = get_gateway_controller(reference_docname) | |||
data = frappe.get_doc("Braintree Settings", gateway_controller).create_payment_request(data) | |||
data = frappe.get_doc("Braintree Settings", gateway_controller).create_payment_request(data) | |||
frappe.db.commit() | |||
return data |
@@ -3,6 +3,7 @@ | |||
import frappe | |||
def get_context(context): | |||
token = frappe.local.form_dict.token | |||
@@ -2,13 +2,14 @@ | |||
# License: MIT. See LICENSE | |||
import frappe | |||
no_cache = True | |||
def get_context(context): | |||
token = frappe.local.form_dict.token | |||
doc = frappe.get_doc(frappe.local.form_dict.doctype, frappe.local.form_dict.docname) | |||
token = frappe.local.form_dict.token | |||
doc = frappe.get_doc(frappe.local.form_dict.doctype, frappe.local.form_dict.docname) | |||
context.payment_message = '' | |||
if hasattr(doc, 'get_payment_success_message'): | |||
context.payment_message = "" | |||
if hasattr(doc, "get_payment_success_message"): | |||
context.payment_message = doc.get_payment_success_message() | |||
@@ -1,16 +1,21 @@ | |||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
import json | |||
import frappe | |||
from frappe import _ | |||
import json | |||
from frappe.integrations.doctype.paytm_settings.paytm_settings import get_paytm_params, get_paytm_config | |||
from frappe.integrations.doctype.paytm_settings.paytm_settings import ( | |||
get_paytm_config, | |||
get_paytm_params, | |||
) | |||
def get_context(context): | |||
context.no_cache = 1 | |||
paytm_config = get_paytm_config() | |||
try: | |||
doc = frappe.get_doc("Integration Request", frappe.form_dict['order_id']) | |||
doc = frappe.get_doc("Integration Request", frappe.form_dict["order_id"]) | |||
context.payment_details = get_paytm_params(json.loads(doc.data), doc.name, paytm_config) | |||
@@ -18,9 +23,12 @@ def get_context(context): | |||
except Exception: | |||
frappe.log_error() | |||
frappe.redirect_to_message(_('Invalid Token'), | |||
_('Seems token you are using is invalid!'), | |||
http_status_code=400, indicator_color='red') | |||
frappe.redirect_to_message( | |||
_("Invalid Token"), | |||
_("Seems token you are using is invalid!"), | |||
http_status_code=400, | |||
indicator_color="red", | |||
) | |||
frappe.local.flags.redirect_location = frappe.local.response.location | |||
raise frappe.Redirect | |||
raise frappe.Redirect |
@@ -1,39 +1,54 @@ | |||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
import json | |||
import frappe | |||
from frappe import _ | |||
from frappe.utils import flt, cint | |||
import json | |||
from frappe.utils import cint, flt | |||
no_cache = 1 | |||
expected_keys = ('amount', 'title', 'description', 'reference_doctype', 'reference_docname', | |||
'payer_name', 'payer_email', 'order_id') | |||
expected_keys = ( | |||
"amount", | |||
"title", | |||
"description", | |||
"reference_doctype", | |||
"reference_docname", | |||
"payer_name", | |||
"payer_email", | |||
"order_id", | |||
) | |||
def get_context(context): | |||
context.no_cache = 1 | |||
context.api_key = get_api_key() | |||
try: | |||
doc = frappe.get_doc("Integration Request", frappe.form_dict['token']) | |||
doc = frappe.get_doc("Integration Request", frappe.form_dict["token"]) | |||
payment_details = json.loads(doc.data) | |||
for key in expected_keys: | |||
context[key] = payment_details[key] | |||
context['token'] = frappe.form_dict['token'] | |||
context['amount'] = flt(context['amount']) | |||
context['subscription_id'] = payment_details['subscription_id'] \ | |||
if payment_details.get('subscription_id') else '' | |||
context["token"] = frappe.form_dict["token"] | |||
context["amount"] = flt(context["amount"]) | |||
context["subscription_id"] = ( | |||
payment_details["subscription_id"] if payment_details.get("subscription_id") else "" | |||
) | |||
except Exception as e: | |||
frappe.redirect_to_message(_('Invalid Token'), | |||
_('Seems token you are using is invalid!'), | |||
http_status_code=400, indicator_color='red') | |||
frappe.redirect_to_message( | |||
_("Invalid Token"), | |||
_("Seems token you are using is invalid!"), | |||
http_status_code=400, | |||
indicator_color="red", | |||
) | |||
frappe.local.flags.redirect_location = frappe.local.response.location | |||
raise frappe.Redirect | |||
def get_api_key(): | |||
api_key = frappe.db.get_value("Razorpay Settings", None, "api_key") | |||
if cint(frappe.form_dict.get("use_sandbox")): | |||
@@ -41,6 +56,7 @@ def get_api_key(): | |||
return api_key | |||
@frappe.whitelist(allow_guest=True) | |||
def make_payment(razorpay_payment_id, options, reference_doctype, reference_docname, token): | |||
data = {} | |||
@@ -48,13 +64,15 @@ def make_payment(razorpay_payment_id, options, reference_doctype, reference_docn | |||
if isinstance(options, str): | |||
data = json.loads(options) | |||
data.update({ | |||
"razorpay_payment_id": razorpay_payment_id, | |||
"reference_docname": reference_docname, | |||
"reference_doctype": reference_doctype, | |||
"token": token | |||
}) | |||
data.update( | |||
{ | |||
"razorpay_payment_id": razorpay_payment_id, | |||
"reference_docname": reference_docname, | |||
"reference_doctype": reference_doctype, | |||
"token": token, | |||
} | |||
) | |||
data = frappe.get_doc("Razorpay Settings").create_request(data) | |||
data = frappe.get_doc("Razorpay Settings").create_request(data) | |||
frappe.db.commit() | |||
return data |
@@ -1,15 +1,26 @@ | |||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
import json | |||
import frappe | |||
from frappe import _ | |||
from frappe.utils import cint, fmt_money | |||
import json | |||
from frappe.integrations.doctype.stripe_settings.stripe_settings import get_gateway_controller | |||
from frappe.utils import cint, fmt_money | |||
no_cache = 1 | |||
expected_keys = ('amount', 'title', 'description', 'reference_doctype', 'reference_docname', | |||
'payer_name', 'payer_email', 'order_id', 'currency') | |||
expected_keys = ( | |||
"amount", | |||
"title", | |||
"description", | |||
"reference_doctype", | |||
"reference_docname", | |||
"payer_name", | |||
"payer_email", | |||
"order_id", | |||
"currency", | |||
) | |||
def get_context(context): | |||
context.no_cache = 1 | |||
@@ -23,20 +34,25 @@ def get_context(context): | |||
context.publishable_key = get_api_key(context.reference_docname, gateway_controller) | |||
context.image = get_header_image(context.reference_docname, gateway_controller) | |||
context['amount'] = fmt_money(amount=context['amount'], currency=context['currency']) | |||
context["amount"] = fmt_money(amount=context["amount"], currency=context["currency"]) | |||
if is_a_subscription(context.reference_doctype, context.reference_docname): | |||
payment_plan = frappe.db.get_value(context.reference_doctype, context.reference_docname, "payment_plan") | |||
payment_plan = frappe.db.get_value( | |||
context.reference_doctype, context.reference_docname, "payment_plan" | |||
) | |||
recurrence = frappe.db.get_value("Payment Plan", payment_plan, "recurrence") | |||
context['amount'] = context['amount'] + " " + _(recurrence) | |||
context["amount"] = context["amount"] + " " + _(recurrence) | |||
else: | |||
frappe.redirect_to_message(_('Some information is missing'), | |||
_('Looks like someone sent you to an incomplete URL. Please ask them to look into it.')) | |||
frappe.redirect_to_message( | |||
_("Some information is missing"), | |||
_("Looks like someone sent you to an incomplete URL. Please ask them to look into it."), | |||
) | |||
frappe.local.flags.redirect_location = frappe.local.response.location | |||
raise frappe.Redirect | |||
def get_api_key(doc, gateway_controller): | |||
publishable_key = frappe.db.get_value("Stripe Settings", gateway_controller, "publishable_key") | |||
if cint(frappe.form_dict.get("use_sandbox")): | |||
@@ -44,31 +60,32 @@ def get_api_key(doc, gateway_controller): | |||
return publishable_key | |||
def get_header_image(doc, gateway_controller): | |||
header_image = frappe.db.get_value("Stripe Settings", gateway_controller, "header_img") | |||
return header_image | |||
@frappe.whitelist(allow_guest=True) | |||
def make_payment(stripe_token_id, data, reference_doctype=None, reference_docname=None): | |||
data = json.loads(data) | |||
data.update({ | |||
"stripe_token_id": stripe_token_id | |||
}) | |||
data.update({"stripe_token_id": stripe_token_id}) | |||
gateway_controller = get_gateway_controller(reference_doctype,reference_docname) | |||
gateway_controller = get_gateway_controller(reference_doctype, reference_docname) | |||
if is_a_subscription(reference_doctype, reference_docname): | |||
reference = frappe.get_doc(reference_doctype, reference_docname) | |||
data = reference.create_subscription("stripe", gateway_controller, data) | |||
data = reference.create_subscription("stripe", gateway_controller, data) | |||
else: | |||
data = frappe.get_doc("Stripe Settings", gateway_controller).create_request(data) | |||
data = frappe.get_doc("Stripe Settings", gateway_controller).create_request(data) | |||
frappe.db.commit() | |||
return data | |||
def is_a_subscription(reference_doctype, reference_docname): | |||
if not frappe.get_meta(reference_doctype).has_field('is_a_subscription'): | |||
if not frappe.get_meta(reference_doctype).has_field("is_a_subscription"): | |||
return False | |||
return frappe.db.get_value(reference_doctype, reference_docname, "is_a_subscription") | |||
return frappe.db.get_value(reference_doctype, reference_docname, "is_a_subscription") |