@@ -0,0 +1,6 @@ | |||||
// Copyright (c) 2018, Frappe Technologies and contributors | |||||
// For license information, please see license.txt | |||||
frappe.ui.form.on('Braintree Settings', { | |||||
}); |
@@ -0,0 +1,273 @@ | |||||
{ | |||||
"allow_copy": 0, | |||||
"allow_guest_to_view": 0, | |||||
"allow_import": 0, | |||||
"allow_rename": 0, | |||||
"autoname": "field:gateway_name", | |||||
"beta": 0, | |||||
"creation": "2018-02-05 13:46:12.101852", | |||||
"custom": 0, | |||||
"docstatus": 0, | |||||
"doctype": "DocType", | |||||
"document_type": "", | |||||
"editable_grid": 1, | |||||
"engine": "InnoDB", | |||||
"fields": [ | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "gateway_name", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Payment Gateway Name", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"options": "Company", | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "section_break_2", | |||||
"fieldtype": "Section Break", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "merchant_id", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Merchant ID", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "public_key", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 1, | |||||
"in_standard_filter": 0, | |||||
"label": "Public Key", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "private_key", | |||||
"fieldtype": "Password", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 1, | |||||
"in_standard_filter": 0, | |||||
"label": "Private Key", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "use_sandbox", | |||||
"fieldtype": "Check", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Use Sandbox", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "header_img", | |||||
"fieldtype": "Attach Image", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Header Image", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
} | |||||
], | |||||
"has_web_view": 0, | |||||
"hide_heading": 0, | |||||
"hide_toolbar": 0, | |||||
"idx": 0, | |||||
"image_view": 0, | |||||
"in_create": 0, | |||||
"is_submittable": 0, | |||||
"issingle": 0, | |||||
"istable": 0, | |||||
"max_attachments": 0, | |||||
"modified": "2018-02-05 14:33:06.050377", | |||||
"modified_by": "Administrator", | |||||
"module": "Integrations", | |||||
"name": "Braintree Settings", | |||||
"name_case": "", | |||||
"owner": "Administrator", | |||||
"permissions": [ | |||||
{ | |||||
"amend": 0, | |||||
"apply_user_permissions": 0, | |||||
"cancel": 0, | |||||
"create": 1, | |||||
"delete": 1, | |||||
"email": 1, | |||||
"export": 0, | |||||
"if_owner": 0, | |||||
"import": 0, | |||||
"permlevel": 0, | |||||
"print": 1, | |||||
"read": 1, | |||||
"report": 0, | |||||
"role": "System Manager", | |||||
"set_user_permissions": 0, | |||||
"share": 1, | |||||
"submit": 0, | |||||
"write": 1 | |||||
} | |||||
], | |||||
"quick_entry": 1, | |||||
"read_only": 0, | |||||
"read_only_onload": 0, | |||||
"show_name_in_global_search": 0, | |||||
"sort_field": "modified", | |||||
"sort_order": "DESC", | |||||
"track_changes": 0, | |||||
"track_seen": 0 | |||||
} |
@@ -0,0 +1,139 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# Copyright (c) 2018, Frappe Technologies and contributors | |||||
# For license information, please see license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
from frappe.model.document import Document | |||||
import braintree | |||||
from frappe import _ | |||||
from six.moves.urllib.parse import urlencode | |||||
from frappe.utils import get_url, call_hook_method | |||||
from frappe.integrations.utils import create_request_log, create_payment_gateway | |||||
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" | |||||
] | |||||
def validate(self): | |||||
if not self.flags.ignore_mandatory: | |||||
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) | |||||
def configure_braintree(self): | |||||
if self.use_sandbox: | |||||
environment = 'sandbox' | |||||
else: | |||||
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) | |||||
) | |||||
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)) | |||||
def get_payment_url(self, **kwargs): | |||||
return get_url("./integrations/braintree_checkout?{0}".format(urlencode(kwargs))) | |||||
def create_payment_request(self, data): | |||||
self.data = frappe._dict(data) | |||||
try: | |||||
self.integration_request = create_request_log(self.data, "Host", "Braintree") | |||||
return self.create_charge_on_braintree() | |||||
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 | |||||
} | |||||
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 | |||||
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.flags.status_changed_to = "Completed" | |||||
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) | |||||
else: | |||||
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) | |||||
if self.flags.status_changed_to == "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') | |||||
if braintree_success_page: | |||||
custom_redirect_to = frappe.get_attr(braintree_success_page[-1])(self.data) | |||||
except Exception: | |||||
frappe.log_error(frappe.get_traceback()) | |||||
if custom_redirect_to: | |||||
redirect_to = custom_redirect_to | |||||
redirect_url = 'payment-success' | |||||
else: | |||||
status = 'Error' | |||||
redirect_url = 'payment-failed' | |||||
if redirect_to: | |||||
redirect_url += '?' + urlencode({'redirect_to': redirect_to}) | |||||
if redirect_message: | |||||
redirect_url += '&' + urlencode({'redirect_message': redirect_message}) | |||||
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") | |||||
return gateway_controller | |||||
def get_client_token(doc): | |||||
gateway_controller = get_gateway_controller(doc) | |||||
settings = frappe.get_doc("Braintree Settings", gateway_controller) | |||||
settings.configure_braintree() | |||||
return braintree.ClientToken.generate() |
@@ -0,0 +1,23 @@ | |||||
/* eslint-disable */ | |||||
// rename this file from _test_[name] to test_[name] to activate | |||||
// and remove above this line | |||||
QUnit.test("test: Braintree Settings", function (assert) { | |||||
let done = assert.async(); | |||||
// number of asserts | |||||
assert.expect(1); | |||||
frappe.run_serially([ | |||||
// insert a new Braintree Setting | |||||
() => frappe.tests.make('Braintree Settings', [ | |||||
// values to be set | |||||
{key: 'value'} | |||||
]), | |||||
() => { | |||||
assert.equal(cur_frm.doc.key, 'value'); | |||||
}, | |||||
() => done() | |||||
]); | |||||
}); |
@@ -0,0 +1,9 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# Copyright (c) 2018, Frappe Technologies and Contributors | |||||
# See license.txt | |||||
from __future__ import unicode_literals | |||||
import unittest | |||||
class TestBraintreeSettings(unittest.TestCase): | |||||
pass |
@@ -158,7 +158,7 @@ | |||||
"idx": 0, | "idx": 0, | ||||
"image_view": 0, | "image_view": 0, | ||||
"in_create": 1, | "in_create": 1, | ||||
"in_dialog": 0, | |||||
"is_submittable": 0, | "is_submittable": 0, | ||||
"issingle": 1, | "issingle": 1, | ||||
"istable": 0, | "istable": 0, | ||||
@@ -28,7 +28,15 @@ Example: | |||||
"payer_name": "Nuran Verkleij", | "payer_name": "Nuran Verkleij", | ||||
"order_id": "111", | "order_id": "111", | ||||
"currency": "USD", | "currency": "USD", | ||||
"payment_gateway": "Razorpay" | |||||
"payment_gateway": "Razorpay", | |||||
"subscription_details": { | |||||
"plan_id": "plan_12313", # if Required | |||||
"start_date": "2018-08-30", | |||||
"billing_period": "Month" #(Day, Week, SemiMonth, Month, Year), | |||||
"billing_frequency": 1, | |||||
"customer_notify": 1, | |||||
"upfront_amount": 1000 | |||||
} | |||||
} | } | ||||
# redirect the user to this url | # redirect the user to this url | ||||
@@ -58,11 +66,15 @@ More Details: | |||||
from __future__ import unicode_literals | from __future__ import unicode_literals | ||||
import frappe | import frappe | ||||
import json | import json | ||||
import pytz | |||||
from frappe import _ | from frappe import _ | ||||
from frappe.utils import get_url, call_hook_method, cint | |||||
from six.moves.urllib.parse import urlencode | from six.moves.urllib.parse import urlencode | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
from frappe.integrations.utils import create_request_log, make_post_request, create_payment_gateway | 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 | |||||
api_path = '/api/method/frappe.integrations.doctype.paypal_settings.paypal_settings' | |||||
class PayPalSettings(Document): | class PayPalSettings(Document): | ||||
supported_currencies = ["AUD", "BRL", "CAD", "CZK", "DKK", "EUR", "HKD", "HUF", "ILS", "JPY", "MYR", "MXN", | supported_currencies = ["AUD", "BRL", "CAD", "CZK", "DKK", "EUR", "HKD", "HUF", "ILS", "JPY", "MYR", "MXN", | ||||
@@ -124,7 +136,7 @@ class PayPalSettings(Document): | |||||
def get_payment_url(self, **kwargs): | def get_payment_url(self, **kwargs): | ||||
setattr(self, "use_sandbox", cint(kwargs.get("use_sandbox", 0))) | setattr(self, "use_sandbox", cint(kwargs.get("use_sandbox", 0))) | ||||
response = self.execute_set_express_checkout(kwargs["amount"], kwargs["currency"]) | |||||
response = self.execute_set_express_checkout(**kwargs) | |||||
if self.paypal_sandbox or self.use_sandbox: | if self.paypal_sandbox or self.use_sandbox: | ||||
return_url = "https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token={0}" | return_url = "https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token={0}" | ||||
@@ -135,30 +147,71 @@ class PayPalSettings(Document): | |||||
"token": response.get("TOKEN")[0], | "token": response.get("TOKEN")[0], | ||||
"correlation_id": response.get("CORRELATIONID")[0] | "correlation_id": response.get("CORRELATIONID")[0] | ||||
}) | }) | ||||
self.integration_request = create_request_log(kwargs, "Remote", "PayPal", response.get("TOKEN")[0]) | self.integration_request = create_request_log(kwargs, "Remote", "PayPal", response.get("TOKEN")[0]) | ||||
return return_url.format(kwargs["token"]) | return return_url.format(kwargs["token"]) | ||||
def execute_set_express_checkout(self, amount, currency): | |||||
def execute_set_express_checkout(self, **kwargs): | |||||
params, url = self.get_paypal_params_and_url() | params, url = self.get_paypal_params_and_url() | ||||
params.update({ | params.update({ | ||||
"METHOD": "SetExpressCheckout", | "METHOD": "SetExpressCheckout", | ||||
"returnUrl": get_url("{0}.get_express_checkout_details".format(api_path)), | |||||
"cancelUrl": get_url("/payment-cancel"), | |||||
"PAYMENTREQUEST_0_PAYMENTACTION": "SALE", | "PAYMENTREQUEST_0_PAYMENTACTION": "SALE", | ||||
"PAYMENTREQUEST_0_AMT": amount, | |||||
"PAYMENTREQUEST_0_CURRENCYCODE": currency.upper(), | |||||
"returnUrl": get_url("/api/method/frappe.integrations.doctype.paypal_settings.paypal_settings.get_express_checkout_details"), | |||||
"cancelUrl": get_url("/payment-cancel") | |||||
"PAYMENTREQUEST_0_AMT": kwargs['amount'], | |||||
"PAYMENTREQUEST_0_CURRENCYCODE": kwargs['currency'].upper() | |||||
}) | }) | ||||
params = urlencode(params) | |||||
if kwargs.get('subscription_details'): | |||||
self.configure_recurring_payments(params, kwargs) | |||||
params = urlencode(params) | |||||
response = make_post_request(url, data=params.encode("utf-8")) | response = make_post_request(url, data=params.encode("utf-8")) | ||||
if response.get("ACK")[0] != "Success": | if response.get("ACK")[0] != "Success": | ||||
frappe.throw(_("Looks like something is wrong with this site's Paypal configuration.")) | frappe.throw(_("Looks like something is wrong with this site's Paypal configuration.")) | ||||
return response | return response | ||||
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'): | |||||
del params[param] | |||||
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") | |||||
doc.setup_sandbox_env(token) | |||||
params, url = doc.get_paypal_params_and_url() | |||||
integration_request = frappe.get_doc("Integration Request", token) | |||||
data = json.loads(integration_request.data) | |||||
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 | |||||
if custom_redirect_to: | |||||
redirect_to = custom_redirect_to | |||||
if redirect_to: | |||||
redirect_url += '&' + urlencode({'redirect_to': redirect_to}) | |||||
if 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) | @frappe.whitelist(allow_guest=True, xss_safe=True) | ||||
def get_express_checkout_details(token): | def get_express_checkout_details(token): | ||||
try: | try: | ||||
@@ -181,14 +234,14 @@ def get_express_checkout_details(token): | |||||
return | return | ||||
doc = frappe.get_doc("Integration Request", token) | |||||
update_integration_request_status(token, { | update_integration_request_status(token, { | ||||
"payerid": response.get("PAYERID")[0], | "payerid": response.get("PAYERID")[0], | ||||
"payer_email": response.get("EMAIL")[0] | "payer_email": response.get("EMAIL")[0] | ||||
}, "Authorized") | |||||
}, "Authorized", doc=doc) | |||||
frappe.local.response["type"] = "redirect" | frappe.local.response["type"] = "redirect" | ||||
frappe.local.response["location"] = get_url( \ | |||||
"/api/method/frappe.integrations.doctype.paypal_settings.paypal_settings.confirm_payment?token={0}".format(token)) | |||||
frappe.local.response["location"] = get_redirect_uri(doc, token, response.get("PAYERID")[0]) | |||||
except Exception: | except Exception: | ||||
frappe.log_error(frappe.get_traceback()) | frappe.log_error(frappe.get_traceback()) | ||||
@@ -196,19 +249,9 @@ def get_express_checkout_details(token): | |||||
@frappe.whitelist(allow_guest=True, xss_safe=True) | @frappe.whitelist(allow_guest=True, xss_safe=True) | ||||
def confirm_payment(token): | def confirm_payment(token): | ||||
try: | try: | ||||
redirect = True | |||||
status_changed_to, redirect_to = None, None | |||||
custom_redirect_to = None | |||||
data, params, url = get_paypal_and_transaction_details(token) | |||||
doc = frappe.get_doc("PayPal Settings") | |||||
doc.setup_sandbox_env(token) | |||||
integration_request = frappe.get_doc("Integration Request", token) | |||||
data = json.loads(integration_request.data) | |||||
redirect_to = data.get('redirect_to') or None | |||||
redirect_message = data.get('redirect_message') or None | |||||
params, url = doc.get_paypal_params_and_url() | |||||
params.update({ | params.update({ | ||||
"METHOD": "DoExpressCheckoutPayment", | "METHOD": "DoExpressCheckoutPayment", | ||||
"PAYERID": data.get("payerid"), | "PAYERID": data.get("payerid"), | ||||
@@ -231,25 +274,148 @@ def confirm_payment(token): | |||||
data.get("reference_docname")).run_method("on_payment_authorized", "Completed") | data.get("reference_docname")).run_method("on_payment_authorized", "Completed") | ||||
frappe.db.commit() | frappe.db.commit() | ||||
if custom_redirect_to: | |||||
redirect_to = custom_redirect_to | |||||
redirect_url = '/integrations/payment-success' | |||||
redirect_url = '/integrations/payment-success?doctype={0}&docname={1}'.format(data.get("reference_doctype"), data.get("reference_docname")) | |||||
else: | else: | ||||
redirect_url = "/integrations/payment-failed" | redirect_url = "/integrations/payment-failed" | ||||
if redirect_to: | |||||
redirect_url += '?' + urlencode({'redirect_to': redirect_to}) | |||||
if redirect_message: | |||||
redirect_url += '&' + urlencode({'redirect_message': redirect_message}) | |||||
setup_redirect(data, redirect_url, custom_redirect_to) | |||||
except Exception: | |||||
frappe.log_error(frappe.get_traceback()) | |||||
@frappe.whitelist(allow_guest=True, xss_safe=True) | |||||
def create_recurring_profile(token, payerid): | |||||
try: | |||||
custom_redirect_to = None | |||||
updating = False | |||||
data, params, url = get_paypal_and_transaction_details(token) | |||||
addons = data.get("addons") | |||||
subscription_details = data.get("subscription_details") | |||||
if data.get('subscription_id') and 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' | |||||
# 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) | |||||
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) | |||||
#"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") | |||||
if data.get("reference_doctype") and data.get("reference_docname"): | |||||
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) | |||||
frappe.db.commit() | |||||
redirect_url = '/integrations/payment-success?doctype={0}&docname={1}'.format(data.get("reference_doctype"), data.get("reference_docname")) | |||||
else: | |||||
redirect_url = "/integrations/payment-failed" | |||||
setup_redirect(data, redirect_url, custom_redirect_to) | |||||
except Exception: | except Exception: | ||||
frappe.log_error(frappe.get_traceback()) | frappe.log_error(frappe.get_traceback()) | ||||
def update_integration_request_status(token, data, status, error=False): | |||||
frappe.get_doc("Integration Request", token).update_status(data, status) | |||||
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)) | |||||
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 | |||||
}) | |||||
response = make_post_request(url, data=args) | |||||
if response.get("ACK")[0] != "Success": | |||||
frappe.throw(_("Failed while amending subscription")) | |||||
@frappe.whitelist(allow_guest=True) | |||||
def ipn_handler(): | |||||
try: | |||||
data = frappe.local.form_dict | |||||
validate_ipn_request(data) | |||||
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) | |||||
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}) | |||||
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) | |||||
if not data.get("recurring_payment_id"): | |||||
_throw() | |||||
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 = urlencode(params) | |||||
res = make_post_request(url=url, data=params.encode("utf-8")) | |||||
if res['ACK'][0] != 'Success': | |||||
_throw() | |||||
def handle_subscription_notification(doctype, docname): | |||||
call_hook_method("handle_subscription_notification", doctype=doctype, docname=docname) |
@@ -101,7 +101,7 @@ | |||||
"idx": 0, | "idx": 0, | ||||
"image_view": 0, | "image_view": 0, | ||||
"in_create": 1, | "in_create": 1, | ||||
"in_dialog": 0, | |||||
"is_submittable": 0, | "is_submittable": 0, | ||||
"issingle": 1, | "issingle": 1, | ||||
"istable": 0, | "istable": 0, | ||||
@@ -28,7 +28,15 @@ Example: | |||||
"payer_name": "Nuran Verkleij", | "payer_name": "Nuran Verkleij", | ||||
"order_id": "111", | "order_id": "111", | ||||
"currency": "INR", | "currency": "INR", | ||||
"payment_gateway": "Razorpay" | |||||
"payment_gateway": "Razorpay", | |||||
"subscription_details": { | |||||
"plan_id": "plan_12313", # if Required | |||||
"start_date": "2018-08-30", | |||||
"billing_period": "Month" #(Day, Week, Month, Year), | |||||
"billing_frequency": 1, | |||||
"customer_notify": 1, | |||||
"upfront_amount": 1000 | |||||
} | |||||
} | } | ||||
# Redirect the user to this url | # Redirect the user to this url | ||||
@@ -58,8 +66,9 @@ from frappe import _ | |||||
import json | import json | ||||
from six.moves.urllib.parse import urlencode | from six.moves.urllib.parse import urlencode | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
from frappe.utils import get_url, call_hook_method, cint | |||||
from frappe.integrations.utils import make_get_request, make_post_request, create_request_log, create_payment_gateway | |||||
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) | |||||
class RazorpaySettings(Document): | class RazorpaySettings(Document): | ||||
supported_currencies = ["INR"] | supported_currencies = ["INR"] | ||||
@@ -82,6 +91,89 @@ class RazorpaySettings(Document): | |||||
if currency not in self.supported_currencies: | 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) | |||||
} | |||||
""" | |||||
url = "https://api.razorpay.com/v1/subscriptions/{0}/addons".format(kwargs.get('subscription_id')) | |||||
try: | |||||
if not frappe.conf.converted_rupee_to_paisa: | |||||
convert_rupee_to_paisa(**kwargs) | |||||
for addon in kwargs.get("addons"): | |||||
resp = make_post_request( | |||||
url, | |||||
auth=(settings.api_key, settings.api_secret), | |||||
data=json.dumps(addon), | |||||
headers={ | |||||
"content-type": "application/json" | |||||
} | |||||
) | |||||
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 | |||||
subscription_details = { | |||||
"plan_id": kwargs.get('subscription_details').get("plan_id"), | |||||
"start_at": cint(start_date), | |||||
"total_count": kwargs.get('subscription_details').get("billing_frequency"), | |||||
"customer_notify": kwargs.get('subscription_details').get("customer_notify") | |||||
} | |||||
if kwargs.get('addons'): | |||||
convert_rupee_to_paisa(**kwargs) | |||||
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" | |||||
} | |||||
) | |||||
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') | |||||
except: | |||||
frappe.log_error(frappe.get_traceback()) | |||||
# failed | |||||
pass | |||||
def prepare_subscription_details(self, settings, **kwargs): | |||||
if not kwargs.get("subscription_id"): | |||||
kwargs = self.setup_subscription(settings, **kwargs) | |||||
if frappe.flags.status !='created': | |||||
kwargs['subscription_id'] = None | |||||
return kwargs | |||||
def get_payment_url(self, **kwargs): | def get_payment_url(self, **kwargs): | ||||
integration_request = create_request_log(kwargs, "Host", "Razorpay") | integration_request = create_request_log(kwargs, "Host", "Razorpay") | ||||
return get_url("./integrations/razorpay_checkout?token={0}".format(integration_request.name)) | return get_url("./integrations/razorpay_checkout?token={0}".format(integration_request.name)) | ||||
@@ -119,6 +211,23 @@ class RazorpaySettings(Document): | |||||
self.integration_request.update_status(data, 'Authorized') | self.integration_request.update_status(data, 'Authorized') | ||||
self.flags.status_changed_to = "Authorized" | self.flags.status_changed_to = "Authorized" | ||||
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.flags.status_changed_to = "Verified" | |||||
if resp.get("status") == "captured": | |||||
# if subscription starts immediately then | |||||
# razorpay charge the actual amount | |||||
# thus changing status to Completed | |||||
self.integration_request.update_status(data, 'Completed') | |||||
self.flags.status_changed_to = "Completed" | |||||
else: | else: | ||||
frappe.log_error(str(resp), 'Razorpay Payment not authorized') | frappe.log_error(str(resp), 'Razorpay Payment not authorized') | ||||
@@ -132,24 +241,26 @@ class RazorpaySettings(Document): | |||||
redirect_to = data.get('notes', {}).get('redirect_to') or None | redirect_to = data.get('notes', {}).get('redirect_to') or None | ||||
redirect_message = data.get('notes', {}).get('redirect_message') or None | redirect_message = data.get('notes', {}).get('redirect_message') or None | ||||
if self.flags.status_changed_to == "Authorized": | |||||
if self.flags.status_changed_to in ("Authorized", "Verified", "Completed"): | |||||
if self.data.reference_doctype and self.data.reference_docname: | if self.data.reference_doctype and self.data.reference_docname: | ||||
custom_redirect_to = None | custom_redirect_to = None | ||||
try: | try: | ||||
frappe.flags.data = data | |||||
custom_redirect_to = frappe.get_doc(self.data.reference_doctype, | custom_redirect_to = frappe.get_doc(self.data.reference_doctype, | ||||
self.data.reference_docname).run_method("on_payment_authorized", self.flags.status_changed_to) | self.data.reference_docname).run_method("on_payment_authorized", self.flags.status_changed_to) | ||||
except Exception: | except Exception: | ||||
frappe.log_error(frappe.get_traceback()) | frappe.log_error(frappe.get_traceback()) | ||||
if custom_redirect_to: | if custom_redirect_to: | ||||
redirect_to = custom_redirect_to | redirect_to = custom_redirect_to | ||||
redirect_url = 'payment-success' | |||||
redirect_url = 'payment-success?doctype={0}&docname={1}'.format(self.data.reference_doctype, self.data.reference_docname) | |||||
else: | else: | ||||
redirect_url = 'payment-failed' | redirect_url = 'payment-failed' | ||||
if redirect_to: | if redirect_to: | ||||
redirect_url += '?' + urlencode({'redirect_to': redirect_to}) | |||||
redirect_url += '&' + urlencode({'redirect_to': redirect_to}) | |||||
if redirect_message: | if redirect_message: | ||||
redirect_url += '&' + urlencode({'redirect_message': redirect_message}) | redirect_url += '&' + urlencode({'redirect_message': redirect_message}) | ||||
@@ -164,7 +275,7 @@ class RazorpaySettings(Document): | |||||
"api_secret": self.get_password(fieldname="api_secret", raise_exception=False) | "api_secret": self.get_password(fieldname="api_secret", raise_exception=False) | ||||
}) | }) | ||||
if cint(data.get('notes', {}).get('use_sandbox')): | |||||
if cint(data.get('notes', {}).get('use_sandbox')) or data.get("use_sandbox"): | |||||
settings.update({ | settings.update({ | ||||
"api_key": frappe.conf.sandbox_api_key, | "api_key": frappe.conf.sandbox_api_key, | ||||
"api_secret": frappe.conf.sandbox_api_secret, | "api_secret": frappe.conf.sandbox_api_secret, | ||||
@@ -172,6 +283,16 @@ class RazorpaySettings(Document): | |||||
return settings | return settings | ||||
def cancel_subscription(self, subscription_id): | |||||
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)) | |||||
except Exception: | |||||
frappe.log_error(frappe.get_traceback()) | |||||
def capture_payment(is_sandbox=False, sanbox_response=None): | def capture_payment(is_sandbox=False, sanbox_response=None): | ||||
""" | """ | ||||
Verifies the purchase as complete by the merchant. | Verifies the purchase as complete by the merchant. | ||||
@@ -201,4 +322,59 @@ def capture_payment(is_sandbox=False, sanbox_response=None): | |||||
doc = frappe.get_doc("Integration Request", doc.name) | doc = frappe.get_doc("Integration Request", doc.name) | ||||
doc.status = "Failed" | doc.status = "Failed" | ||||
doc.error = frappe.get_traceback() | doc.error = frappe.get_traceback() | ||||
frappe.log_error(doc.error, '{0} Failed'.format(doc.name)) | |||||
frappe.log_error(doc.error, '{0} Failed'.format(doc.name)) | |||||
def convert_rupee_to_paisa(**kwargs): | |||||
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: | |||||
data = frappe.local.form_dict | |||||
validate_payment_callback(data) | |||||
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) | |||||
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}) | |||||
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") | |||||
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)) | |||||
if resp.get("status") != "active": | |||||
_throw() | |||||
def handle_subscription_notification(doctype, docname): | |||||
call_hook_method("handle_subscription_notification", doctype=doctype, docname=docname) |
@@ -3,6 +3,7 @@ | |||||
"allow_guest_to_view": 0, | "allow_guest_to_view": 0, | ||||
"allow_import": 0, | "allow_import": 0, | ||||
"allow_rename": 0, | "allow_rename": 0, | ||||
"autoname": "field:gateway_name", | |||||
"beta": 0, | "beta": 0, | ||||
"creation": "2017-03-09 17:18:29.458397", | "creation": "2017-03-09 17:18:29.458397", | ||||
"custom": 0, | "custom": 0, | ||||
@@ -13,6 +14,40 @@ | |||||
"engine": "InnoDB", | "engine": "InnoDB", | ||||
"fields": [ | "fields": [ | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "gateway_name", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 1, | |||||
"in_standard_filter": 0, | |||||
"label": "Payment Gateway Name", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"translatable": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -24,7 +59,7 @@ | |||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | "in_global_search": 0, | ||||
"in_list_view": 0, | |||||
"in_list_view": 1, | |||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"label": "Publishable Key", | "label": "Publishable Key", | ||||
"length": 0, | "length": 0, | ||||
@@ -39,9 +74,43 @@ | |||||
"reqd": 1, | "reqd": 1, | ||||
"search_index": 0, | "search_index": 0, | ||||
"set_only_once": 0, | "set_only_once": 0, | ||||
"translatable": 0, | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "column_break_3", | |||||
"fieldtype": "Column Break", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"translatable": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -53,7 +122,7 @@ | |||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | "in_global_search": 0, | ||||
"in_list_view": 0, | |||||
"in_list_view": 1, | |||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"label": "Secret Key", | "label": "Secret Key", | ||||
"length": 0, | "length": 0, | ||||
@@ -68,6 +137,133 @@ | |||||
"reqd": 1, | "reqd": 1, | ||||
"search_index": 0, | "search_index": 0, | ||||
"set_only_once": 0, | "set_only_once": 0, | ||||
"translatable": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "section_break_5", | |||||
"fieldtype": "Section Break", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"translatable": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "header_img", | |||||
"fieldtype": "Attach Image", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Header Image", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"translatable": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "column_break_7", | |||||
"fieldtype": "Column Break", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"translatable": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "redirect_url", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Redirect URL", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"translatable": 0, | |||||
"unique": 0 | "unique": 0 | ||||
} | } | ||||
], | ], | ||||
@@ -78,10 +274,10 @@ | |||||
"image_view": 0, | "image_view": 0, | ||||
"in_create": 0, | "in_create": 0, | ||||
"is_submittable": 0, | "is_submittable": 0, | ||||
"issingle": 1, | |||||
"issingle": 0, | |||||
"istable": 0, | "istable": 0, | ||||
"max_attachments": 0, | "max_attachments": 0, | ||||
"modified": "2017-03-09 17:19:25.087475", | |||||
"modified": "2018-05-23 13:32:14.429916", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Integrations", | "module": "Integrations", | ||||
"name": "Stripe Settings", | "name": "Stripe Settings", | ||||
@@ -90,7 +286,6 @@ | |||||
"permissions": [ | "permissions": [ | ||||
{ | { | ||||
"amend": 0, | "amend": 0, | ||||
"apply_user_permissions": 0, | |||||
"cancel": 0, | "cancel": 0, | ||||
"create": 1, | "create": 1, | ||||
"delete": 1, | "delete": 1, | ||||
@@ -29,9 +29,9 @@ class StripeSettings(Document): | |||||
'GBP': 0.30, 'NZD': 0.50, 'SGD': 0.50 | 'GBP': 0.30, 'NZD': 0.50, 'SGD': 0.50 | ||||
} | } | ||||
def validate(self): | |||||
create_payment_gateway('Stripe') | |||||
call_hook_method('payment_gateway_enabled', gateway='Stripe') | |||||
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) | |||||
if not self.flags.ignore_mandatory: | if not self.flags.ignore_mandatory: | ||||
self.validate_stripe_credentails() | self.validate_stripe_credentails() | ||||
@@ -55,50 +55,46 @@ class StripeSettings(Document): | |||||
def get_payment_url(self, **kwargs): | def get_payment_url(self, **kwargs): | ||||
return get_url("./integrations/stripe_checkout?{0}".format(urlencode(kwargs))) | return get_url("./integrations/stripe_checkout?{0}".format(urlencode(kwargs))) | ||||
def create_request(self, data): | def create_request(self, data): | ||||
import stripe | |||||
self.data = frappe._dict(data) | 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() | |||||
try: | try: | ||||
self.integration_request = create_request_log(self.data, "Host", "Stripe") | self.integration_request = create_request_log(self.data, "Host", "Stripe") | ||||
return self.create_charge_on_stripe() | return self.create_charge_on_stripe() | ||||
except Exception: | except Exception: | ||||
frappe.log_error(frappe.get_traceback()) | frappe.log_error(frappe.get_traceback()) | ||||
return{ | 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.")), | |||||
"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 | "status": 401 | ||||
} | } | ||||
def create_charge_on_stripe(self): | |||||
headers = {"Authorization": | |||||
"Bearer {0}".format(self.get_password(fieldname="secret_key", raise_exception=False))} | |||||
data = { | |||||
"amount": cint(flt(self.data.amount)*100), | |||||
"currency": self.data.currency, | |||||
"source": self.data.stripe_token_id, | |||||
"description": self.data.description | |||||
} | |||||
redirect_to = self.data.get('redirect_to') or None | |||||
redirect_message = self.data.get('redirect_message') or None | |||||
def create_charge_on_stripe(self): | |||||
import stripe | |||||
try: | try: | ||||
resp = make_post_request(url="https://api.stripe.com/v1/charges", headers=headers, data=data) | |||||
if resp.get("captured") == True: | |||||
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) | |||||
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" | self.flags.status_changed_to = "Completed" | ||||
else: | else: | ||||
frappe.log_error(str(resp), 'Stripe Payment not completed') | |||||
frappe.log_error(charge.failure_message, 'Stripe Payment not completed') | |||||
except: | |||||
except Exception: | |||||
frappe.log_error(frappe.get_traceback()) | frappe.log_error(frappe.get_traceback()) | ||||
# failed | |||||
pass | |||||
status = frappe.flags.integration_request.status_code | |||||
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 | |||||
status = self.integration_request.status | |||||
if self.flags.status_changed_to == "Completed": | if self.flags.status_changed_to == "Completed": | ||||
if self.data.reference_doctype and self.data.reference_docname: | if self.data.reference_doctype and self.data.reference_docname: | ||||
@@ -112,7 +108,11 @@ class StripeSettings(Document): | |||||
if custom_redirect_to: | if custom_redirect_to: | ||||
redirect_to = 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: | else: | ||||
redirect_url = 'payment-failed' | redirect_url = 'payment-failed' | ||||
@@ -125,3 +125,8 @@ class StripeSettings(Document): | |||||
"redirect_to": redirect_url, | "redirect_to": redirect_url, | ||||
"status": status | "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") | |||||
return gateway_controller |
@@ -0,0 +1,23 @@ | |||||
/* eslint-disable */ | |||||
// rename this file from _test_[name] to test_[name] to activate | |||||
// and remove above this line | |||||
QUnit.test("test: Stripe Settings", function (assert) { | |||||
let done = assert.async(); | |||||
// number of asserts | |||||
assert.expect(1); | |||||
frappe.run_serially([ | |||||
// insert a new Stripe Settings | |||||
() => frappe.tests.make('Stripe Settings', [ | |||||
// values to be set | |||||
{key: 'value'} | |||||
]), | |||||
() => { | |||||
assert.equal(cur_frm.doc.key, 'value'); | |||||
}, | |||||
() => done() | |||||
]); | |||||
}); |
@@ -0,0 +1,9 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# Copyright (c) 2018, Frappe Technologies and Contributors | |||||
# See license.txt | |||||
from __future__ import unicode_literals | |||||
import unittest | |||||
class TestStripeSettings(unittest.TestCase): | |||||
pass |
@@ -13,6 +13,7 @@ | |||||
"editable_grid": 1, | "editable_grid": 1, | ||||
"fields": [ | "fields": [ | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -24,7 +25,7 @@ | |||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | "in_global_search": 0, | ||||
"in_list_view": 0, | |||||
"in_list_view": 1, | |||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"label": "Gateway", | "label": "Gateway", | ||||
"length": 0, | "length": 0, | ||||
@@ -40,6 +41,68 @@ | |||||
"search_index": 0, | "search_index": 0, | ||||
"set_only_once": 0, | "set_only_once": 0, | ||||
"unique": 0 | "unique": 0 | ||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "gateway_settings", | |||||
"fieldtype": "Link", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Gateway Settings", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"options": "DocType", | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "gateway_controller", | |||||
"fieldtype": "Dynamic Link", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Gateway Controller", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"options": "gateway_settings", | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
} | } | ||||
], | ], | ||||
"has_web_view": 0, | "has_web_view": 0, | ||||
@@ -52,7 +115,7 @@ | |||||
"issingle": 0, | "issingle": 0, | ||||
"istable": 0, | "istable": 0, | ||||
"max_attachments": 0, | "max_attachments": 0, | ||||
"modified": "2017-03-09 12:40:56.176464", | |||||
"modified": "2018-02-05 14:24:33.526645", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Core", | "module": "Core", | ||||
"name": "Payment Gateway", | "name": "Payment Gateway", | ||||
@@ -0,0 +1,23 @@ | |||||
/* eslint-disable */ | |||||
// rename this file from _test_[name] to test_[name] to activate | |||||
// and remove above this line | |||||
QUnit.test("test: Payment Gateway", function (assert) { | |||||
let done = assert.async(); | |||||
// number of asserts | |||||
assert.expect(1); | |||||
frappe.run_serially([ | |||||
// insert a new Payment Gateway | |||||
() => frappe.tests.make('Payment Gateway', [ | |||||
// values to be set | |||||
{key: 'value'} | |||||
]), | |||||
() => { | |||||
assert.equal(cur_frm.doc.key, 'value'); | |||||
}, | |||||
() => done() | |||||
]); | |||||
}); |
@@ -0,0 +1,55 @@ | |||||
$(document).ready(function() { | |||||
var button = document.querySelector('#submit-button'); | |||||
var form = document.querySelector('#payment-form'); | |||||
var data = {{ frappe.form_dict | json }}; | |||||
var doctype = "{{ reference_doctype }}" | |||||
var docname = "{{ reference_docname }}" | |||||
braintree.dropin.create({ | |||||
authorization: "{{ client_token }}", | |||||
container: '#bt-dropin', | |||||
paypal: { | |||||
flow: 'vault' | |||||
} | |||||
}, function(createErr, instance) { | |||||
form.addEventListener('submit', function(event) { | |||||
event.preventDefault(); | |||||
instance.requestPaymentMethod(function(err, payload) { | |||||
if (err) { | |||||
console.log('Error', err); | |||||
return; | |||||
} | |||||
frappe.call({ | |||||
method: "frappe.templates.pages.integrations.braintree_checkout.make_payment", | |||||
freeze: true, | |||||
headers: { | |||||
"X-Requested-With": "XMLHttpRequest" | |||||
}, | |||||
args: { | |||||
"payload_nonce": payload.nonce, | |||||
"data": JSON.stringify(data), | |||||
"reference_doctype": doctype, | |||||
"reference_docname": docname | |||||
}, | |||||
callback: function(r) { | |||||
if (r.message && r.message.status == "Completed") { | |||||
window.location.href = r.message.redirect_to | |||||
} else if (r.message && r.message.status == "Error") { | |||||
window.location.href = r.message.redirect_to | |||||
} | |||||
} | |||||
}) | |||||
}); | |||||
}); | |||||
instance.on('paymentMethodRequestable', function (event) { | |||||
button.removeAttribute('disabled'); | |||||
}); | |||||
instance.on('noPaymentMethodRequestable', function () { | |||||
button.setAttribute('disabled', true); | |||||
}); | |||||
}); | |||||
}) |
@@ -5,6 +5,7 @@ $(document).ready(function(){ | |||||
"amount": cint({{ amount }} * 100), // 2000 paise = INR 20 | "amount": cint({{ amount }} * 100), // 2000 paise = INR 20 | ||||
"name": "{{ title }}", | "name": "{{ title }}", | ||||
"description": "{{ description }}", | "description": "{{ description }}", | ||||
"subscription_id": "{{ subscription_id }}", | |||||
"handler": function (response){ | "handler": function (response){ | ||||
razorpay.make_payment_log(response, options, "{{ reference_doctype }}", "{{ reference_docname }}", "{{ token }}"); | razorpay.make_payment_log(response, options, "{{ reference_doctype }}", "{{ reference_docname }}", "{{ token }}"); | ||||
}, | }, | ||||
@@ -1,47 +1,85 @@ | |||||
$(document).ready(function(){ | |||||
(function(e){ | |||||
var handler = StripeCheckout.configure({ | |||||
key: "{{ publishable_key }}", | |||||
token: function(token) { | |||||
// You can access the token ID with `token.id`. | |||||
// Get the token ID to your server-side code for use. | |||||
stripe.make_payment_log(token, {{ frappe.form_dict|json }}, "{{ reference_doctype }}", "{{ reference_docname }}"); | |||||
var stripe = Stripe("{{ publishable_key }}"); | |||||
var elements = stripe.elements(); | |||||
var style = { | |||||
base: { | |||||
color: '#32325d', | |||||
lineHeight: '18px', | |||||
fontFamily: '"Helvetica Neue", Helvetica, sans-serif', | |||||
fontSmoothing: 'antialiased', | |||||
fontSize: '16px', | |||||
'::placeholder': { | |||||
color: '#aab7c4' | |||||
} | |||||
}, | |||||
invalid: { | |||||
color: '#fa755a', | |||||
iconColor: '#fa755a' | |||||
} | |||||
}; | |||||
var card = elements.create('card', { | |||||
hidePostalCode: true, | |||||
style: style | |||||
}); | |||||
card.mount('#card-element'); | |||||
function setOutcome(result) { | |||||
if (result.token) { | |||||
$('#submit').prop('disabled', true) | |||||
$('#submit').html(__('Processing...')) | |||||
frappe.call({ | |||||
method:"frappe.templates.pages.integrations.stripe_checkout.make_payment", | |||||
freeze:true, | |||||
headers: {"X-Requested-With": "XMLHttpRequest"}, | |||||
args: { | |||||
"stripe_token_id": result.token.id, | |||||
"data": JSON.stringify({{ frappe.form_dict|json }}), | |||||
"reference_doctype": "{{ reference_doctype }}", | |||||
"reference_docname": "{{ reference_docname }}" | |||||
}, | |||||
callback: function(r) { | |||||
if (r.message.status == "Completed") { | |||||
$('#submit').hide() | |||||
$('.success').show() | |||||
setTimeout(function() { | |||||
window.location.href = r.message.redirect_to | |||||
}, 2000); | |||||
} else { | |||||
$('#submit').hide() | |||||
$('.error').show() | |||||
setTimeout(function() { | |||||
window.location.href = r.message.redirect_to | |||||
}, 2000); | |||||
} | |||||
} | } | ||||
}); | }); | ||||
handler.open({ | |||||
name: "{{payer_name}}", | |||||
description: "{{description}}", | |||||
amount: cint("{{ amount }}" * 100), // 2000 paise = INR 20 | |||||
email: "{{payer_email}}", | |||||
currency: "{{currency}}" | |||||
}); | |||||
})(); | |||||
}) | |||||
frappe.provide('stripe'); | |||||
stripe.make_payment_log = function(token, data, doctype, docname){ | |||||
$('.stripe-loading').addClass('hidden'); | |||||
$('.stripe-confirming').removeClass('hidden'); | |||||
frappe.call({ | |||||
method:"frappe.templates.pages.integrations.stripe_checkout.make_payment", | |||||
freeze:true, | |||||
headers: {"X-Requested-With": "XMLHttpRequest"}, | |||||
args: { | |||||
"stripe_token_id": token.id, | |||||
"data": JSON.stringify(data), | |||||
"reference_doctype": doctype, | |||||
"reference_docname": docname | |||||
}, | |||||
callback: function(r){ | |||||
if (r.message && r.message.status == 200) { | |||||
window.location.href = r.message.redirect_to | |||||
} | |||||
else if (r.message && ([401,400,500].indexOf(r.message.status) > -1)) { | |||||
window.location.href = r.message.redirect_to | |||||
} | |||||
} else if (result.error) { | |||||
$('.error').html(result.error.message); | |||||
$('.error').show() | |||||
} | |||||
} | |||||
card.on('change', function(event) { | |||||
var displayError = document.getElementById('card-errors'); | |||||
if (event.error) { | |||||
displayError.textContent = event.error.message; | |||||
} else { | |||||
displayError.textContent = ''; | |||||
} | |||||
}); | |||||
frappe.ready(function() { | |||||
$('#submit').off("click").on("click", function(e) { | |||||
e.preventDefault(); | |||||
var extraDetails = { | |||||
name: $('input[name=cardholder-name]').val(), | |||||
email: $('input[name=cardholder-email]').val() | |||||
} | } | ||||
stripe.createToken(card, extraDetails).then(setOutcome); | |||||
}) | }) | ||||
} | |||||
}); |
@@ -0,0 +1,54 @@ | |||||
{% extends "templates/web.html" %} | |||||
{% block title %} Payment {% endblock %} | |||||
{%- block header -%}{% endblock %} | |||||
{% block script %} | |||||
<script src="https://js.braintreegateway.com/web/dropin/1.9.3/js/dropin.min.js"></script> | |||||
<script>{% include "templates/includes/integrations/braintree_checkout.js" %}</script> | |||||
{% endblock %} | |||||
{%- block page_content -%} | |||||
<div class="wrapper"> | |||||
<div class="checkout container"> | |||||
<header> | |||||
<div> | |||||
<img class="center" src="{{ header_img }}"></img> | |||||
</div> | |||||
</header> | |||||
<form id="payment-form"> | |||||
<section> | |||||
<div class="bt-drop-in-wrapper"> | |||||
<div id="bt-dropin"></div> | |||||
</div> | |||||
</section> | |||||
<button class="btn btn-primary" type="submit" id="submit-button" disabled><span>{{ _("Pay") }} {{ amount }} {{ currency }}</span></button> | |||||
</form> | |||||
</div> | |||||
</div> | |||||
<style> | |||||
.checkout { | |||||
max-width: 60%; | |||||
} | |||||
.center { | |||||
margin:auto; | |||||
display: block; | |||||
} | |||||
#payment-form { | |||||
margin-top: 40px; | |||||
} | |||||
#submit-button { | |||||
float: right; | |||||
} | |||||
</style> | |||||
{% endblock %} |
@@ -0,0 +1,48 @@ | |||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors | |||||
# License: GNU General Public License v3. See license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
from frappe import _ | |||||
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 | |||||
no_sitemap = 1 | |||||
expected_keys = ('amount', 'title', 'description', 'reference_doctype', 'reference_docname', | |||||
'payer_name', 'payer_email', 'order_id', 'currency') | |||||
def get_context(context): | |||||
context.no_cache = 1 | |||||
# all these keys exist in form_dict | |||||
if not (set(expected_keys) - set(list(frappe.form_dict))): | |||||
for key in expected_keys: | |||||
context[key] = frappe.form_dict[key] | |||||
context.client_token = get_client_token(context.reference_docname) | |||||
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") | |||||
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.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 | |||||
}) | |||||
gateway_controller = get_gateway_controller(reference_docname) | |||||
data = frappe.get_doc("Braintree Settings", gateway_controller).create_payment_request(data) | |||||
frappe.db.commit() | |||||
return data |
@@ -0,0 +1,21 @@ | |||||
{% extends "templates/web.html" %} | |||||
{% block title %}{{ _("Connection Success") }}{% endblock %} | |||||
{%- block page_content -%} | |||||
<div class='page-card'> | |||||
<div class='page-card-head'> | |||||
<span class='indicator green'> | |||||
{{ _("Success") }}</span> | |||||
</div> | |||||
<p>{{ _("Your connection request to Google Calendar was successfully accepted") }}</p> | |||||
<div><a href='{{ "/desk" }}' class='btn btn-primary btn-sm'> | |||||
{{ _("Back to Desk") }}</a></div> | |||||
</div> | |||||
<style> | |||||
.hero-and-content { | |||||
background-color: #f5f7fa; | |||||
} | |||||
{% include "templates/styles/card_style.css" %} | |||||
</style> | |||||
{% endblock %} |
@@ -16,6 +16,7 @@ | |||||
.hero-and-content { | .hero-and-content { | ||||
background-color: #f5f7fa; | background-color: #f5f7fa; | ||||
} | } | ||||
{% include "templates/styles/card_style.css" %} | |||||
</style> | </style> | ||||
{% endblock %} | {% endblock %} |
@@ -16,6 +16,7 @@ | |||||
.hero-and-content { | .hero-and-content { | ||||
background-color: #f5f7fa; | background-color: #f5f7fa; | ||||
} | } | ||||
{% include "templates/styles/card_style.css" %} | |||||
</style> | </style> | ||||
{% endblock %} | {% endblock %} |
@@ -8,13 +8,27 @@ | |||||
<span class='indicator green'> | <span class='indicator green'> | ||||
{{ _("Success") }}</span> | {{ _("Success") }}</span> | ||||
</div> | </div> | ||||
<p>{{ _("Your payment was successfully accepted") }}</p> | |||||
<div><a href='{{ frappe.form_dict.redirect_to or "/" }}' class='btn btn-primary btn-sm'> | |||||
{{ _("Continue") }}</a></div> | |||||
<p>{{ payment_message or _("Your payment was successfully accepted") }}</p> | |||||
{% if not payment_message %} | |||||
<div> | |||||
<a | |||||
href='{{ frappe.form_dict.redirect_to or "/" }}' | |||||
class='btn btn-primary btn-sm'> | |||||
{{ _("Continue") }} | |||||
</a> | |||||
</div> | |||||
{% endif %} | |||||
</div> | </div> | ||||
<style> | <style> | ||||
.hero-and-content { | |||||
background-color: #f5f7fa; | |||||
} | |||||
{% include "templates/styles/card_style.css" %} | |||||
</style> | </style> | ||||
<script> | |||||
frappe.ready(function() { | |||||
if('{{ frappe.form_dict.redirect_to or "" }}'){ | |||||
setTimeout(function(){ | |||||
window.location.href = '{{ frappe.form_dict.redirect_to }}'; | |||||
}, 4000); | |||||
} | |||||
}) | |||||
</script> | |||||
{% endblock %} | {% endblock %} |
@@ -4,6 +4,13 @@ | |||||
from __future__ import unicode_literals | from __future__ import unicode_literals | ||||
import frappe | import frappe | ||||
no_cache = True | |||||
def get_context(context): | def get_context(context): | ||||
token = frappe.local.form_dict.token | |||||
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 = doc.get_payment_success_message() | |||||
@@ -26,8 +26,10 @@ def get_context(context): | |||||
context['token'] = frappe.form_dict['token'] | context['token'] = frappe.form_dict['token'] | ||||
context['amount'] = flt(context['amount']) | context['amount'] = flt(context['amount']) | ||||
context['subscription_id'] = payment_details['subscription_id'] \ | |||||
if payment_details.get('subscription_id') else '' | |||||
except Exception: | |||||
except Exception as e: | |||||
frappe.redirect_to_message(_('Invalid Token'), | frappe.redirect_to_message(_('Invalid Token'), | ||||
_('Seems token you are using is invalid!'), | _('Seems token you are using is invalid!'), | ||||
http_status_code=400, indicator_color='red') | http_status_code=400, indicator_color='red') | ||||
@@ -0,0 +1,113 @@ | |||||
.StripeElement { | |||||
background-color: white; | |||||
height: 40px; | |||||
padding: 10px 12px; | |||||
border-radius: 4px; | |||||
border: 1px solid transparent; | |||||
box-shadow: 0 1px 3px 0 #e6ebf1; | |||||
-webkit-transition: box-shadow 150ms ease; | |||||
transition: box-shadow 150ms ease; | |||||
} | |||||
.StripeElement--focus { | |||||
box-shadow: 0 1px 3px 0 #cfd7df; | |||||
} | |||||
.StripeElement--invalid { | |||||
border-color: #fa755a; | |||||
} | |||||
.StripeElement--webkit-autofill { | |||||
background-color: #fefde5; | |||||
} | |||||
.stripe #payment-form { | |||||
margin-top: 80px; | |||||
} | |||||
.stripe button { | |||||
float: right; | |||||
display: block; | |||||
background: #5e64ff; | |||||
color: white; | |||||
box-shadow: 0 7px 14px 0 rgba(49, 49, 93, 0.10), 0 3px 6px 0 rgba(0, 0, 0, 0.08); | |||||
border-radius: 4px; | |||||
border: 0; | |||||
margin-top: 20px; | |||||
font-size: 15px; | |||||
font-weight: 400; | |||||
max-width: 40%; | |||||
height: 40px; | |||||
line-height: 38px; | |||||
outline: none; | |||||
} | |||||
.stripe button:hover, .stripe button:focus { | |||||
background: #2b33ff; | |||||
border-color: #0711ff; | |||||
} | |||||
.stripe button:active { | |||||
background: #5e64ff; | |||||
} | |||||
.stripe button:disabled { | |||||
background: #515e80; | |||||
} | |||||
.stripe .group { | |||||
background: white; | |||||
box-shadow: 2px 7px 14px 2px rgba(49, 49, 93, 0.10), 0 3px 6px 0 rgba(0, 0, 0, 0.08); | |||||
border-radius: 4px; | |||||
margin-bottom: 20px; | |||||
} | |||||
.stripe label { | |||||
position: relative; | |||||
color: #8898AA; | |||||
font-weight: 300; | |||||
height: 40px; | |||||
line-height: 40px; | |||||
margin-left: 20px; | |||||
display: block; | |||||
} | |||||
.stripe .group label:not(:last-child) { | |||||
border-bottom: 1px solid #F0F5FA; | |||||
} | |||||
.stripe label>span { | |||||
width: 20%; | |||||
text-align: right; | |||||
float: left; | |||||
} | |||||
.current-card { | |||||
margin-left: 20px; | |||||
} | |||||
.field { | |||||
background: transparent; | |||||
font-weight: 300; | |||||
border: 0; | |||||
color: #31325F; | |||||
outline: none; | |||||
padding-right: 10px; | |||||
padding-left: 10px; | |||||
cursor: text; | |||||
width: 70%; | |||||
height: 40px; | |||||
float: right; | |||||
} | |||||
.field::-webkit-input-placeholder { | |||||
color: #CFD7E0; | |||||
} | |||||
.field::-moz-placeholder { | |||||
color: #CFD7E0; | |||||
} | |||||
.field:-ms-input-placeholder { | |||||
color: #CFD7E0; | |||||
} |
@@ -2,27 +2,57 @@ | |||||
{% block title %} Payment {% endblock %} | {% block title %} Payment {% endblock %} | ||||
{%- block header -%}{% endblock %} | |||||
{%- block header -%} | |||||
{% endblock %} | |||||
{% block script %} | {% block script %} | ||||
<script src="https://checkout.stripe.com/checkout.js"></script> | |||||
<script src="https://js.stripe.com/v3/"></script> | |||||
<script>{% include "templates/includes/integrations/stripe_checkout.js" %}</script> | <script>{% include "templates/includes/integrations/stripe_checkout.js" %}</script> | ||||
{% endblock %} | {% endblock %} | ||||
{%- block page_content -%} | {%- block page_content -%} | ||||
<p class='lead text-center centered'> | |||||
<span class='stripe-loading'>Loading Payment System</span> | |||||
<span class='stripe-confirming hidden'>Confirming Payment</span> | |||||
</p> | |||||
{% endblock %} | |||||
<div class="row stripe" style="min-height: 400px; padding-bottom: 50px; margin-top:100px;"> | |||||
<div class="col-sm-8 col-sm-offset-2"> | |||||
{% if image %} | |||||
<img src={{image}}> | |||||
{% endif %} | |||||
<h2 class="text-center">{{description}}</h2> | |||||
<form id="payment-form"> | |||||
<div class="form-row"> | |||||
<div class="group"> | |||||
<div> | |||||
<label> | |||||
<span>{{ _("Name") }}</span> | |||||
<input id="cardholder-name" name="cardholder-name" class="field" placeholder="{{ _('John Doe') }}" value="{{payer_name}}"/> | |||||
</label> | |||||
</div> | |||||
</div> | |||||
<div class="group"> | |||||
<div> | |||||
<label> | |||||
<span>{{ _("Email") }}</span> | |||||
<input id="cardholder-email" name="cardholder-email" class="field" placeholder="{{ _('john@doe.com') }}" value="{{payer_email}}"/> | |||||
</label> | |||||
</div> | |||||
</div> | |||||
<div class="group"> | |||||
<label> | |||||
<span>{{ _("Card Details") }}</span> | |||||
<div id="card-element" name="card-element" class="field"></div> | |||||
<div id="card-errors" role="alert"></div> | |||||
</label> | |||||
</div> | |||||
</div> | |||||
<button type="submit" class="submit" id="submit">{{_('Pay')}} {{amount}}</button> | |||||
<div class="outcome text-center"> | |||||
<div class="error" hidden>{{ _("An error occured during the payment process. Please contact us.") }}</div> | |||||
<div class="success" hidden>{{ _("Your payment has been successfully registered.") }}</div> | |||||
</div> | |||||
</form> | |||||
</div> | |||||
</div> | |||||
{% block style %} | |||||
<style> | |||||
header, footer { | |||||
display: none; | |||||
} | |||||
</style> | |||||
{% endblock %} | |||||
{% endblock %} |
@@ -3,8 +3,9 @@ | |||||
from __future__ import unicode_literals | from __future__ import unicode_literals | ||||
import frappe | import frappe | ||||
from frappe import _ | from frappe import _ | ||||
from frappe.utils import flt, cint | |||||
from frappe.utils import cint, fmt_money | |||||
import json | import json | ||||
from frappe.integrations.doctype.stripe_settings.stripe_settings import get_gateway_controller | |||||
no_cache = 1 | no_cache = 1 | ||||
no_sitemap = 1 | no_sitemap = 1 | ||||
@@ -14,14 +15,23 @@ expected_keys = ('amount', 'title', 'description', 'reference_doctype', 'referen | |||||
def get_context(context): | def get_context(context): | ||||
context.no_cache = 1 | context.no_cache = 1 | ||||
context.publishable_key = get_api_key() | |||||
# all these keys exist in form_dict | # all these keys exist in form_dict | ||||
if not (set(expected_keys) - set(frappe.form_dict.keys())): | |||||
if not (set(expected_keys) - set(list(frappe.form_dict))): | |||||
for key in expected_keys: | for key in expected_keys: | ||||
context[key] = frappe.form_dict[key] | context[key] = frappe.form_dict[key] | ||||
context['amount'] = flt(context['amount']) | |||||
gateway_controller = get_gateway_controller(context.reference_doctype, context.reference_docname) | |||||
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']) | |||||
if frappe.db.get_value(context.reference_doctype, context.reference_docname, "is_a_subscription"): | |||||
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) | |||||
else: | else: | ||||
frappe.redirect_to_message(_('Some information is missing'), | frappe.redirect_to_message(_('Some information is missing'), | ||||
@@ -29,13 +39,18 @@ def get_context(context): | |||||
frappe.local.flags.redirect_location = frappe.local.response.location | frappe.local.flags.redirect_location = frappe.local.response.location | ||||
raise frappe.Redirect | raise frappe.Redirect | ||||
def get_api_key(): | |||||
publishable_key = frappe.db.get_value("Stripe Settings", None, "publishable_key") | |||||
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")): | if cint(frappe.form_dict.get("use_sandbox")): | ||||
publishable_key = frappe.conf.sandbox_publishable_key | publishable_key = frappe.conf.sandbox_publishable_key | ||||
return publishable_key | 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) | @frappe.whitelist(allow_guest=True) | ||||
def make_payment(stripe_token_id, data, reference_doctype=None, reference_docname=None): | def make_payment(stripe_token_id, data, reference_doctype=None, reference_docname=None): | ||||
data = json.loads(data) | data = json.loads(data) | ||||
@@ -44,6 +59,13 @@ def make_payment(stripe_token_id, data, reference_doctype=None, reference_docnam | |||||
"stripe_token_id": stripe_token_id | "stripe_token_id": stripe_token_id | ||||
}) | }) | ||||
data = frappe.get_doc("Stripe Settings").create_request(data) | |||||
gateway_controller = get_gateway_controller(reference_doctype,reference_docname) | |||||
if frappe.db.get_value(reference_doctype, reference_docname, 'is_a_subscription'): | |||||
reference = frappe.get_doc(reference_doctype, reference_docname) | |||||
data = reference.create_subscription("stripe", gateway_controller, data) | |||||
else: | |||||
data = frappe.get_doc("Stripe Settings", gateway_controller).create_request(data) | |||||
frappe.db.commit() | frappe.db.commit() | ||||
return data | |||||
return data |