@@ -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, | |||
"image_view": 0, | |||
"in_create": 1, | |||
"in_dialog": 0, | |||
"is_submittable": 0, | |||
"issingle": 1, | |||
"istable": 0, | |||
@@ -28,7 +28,15 @@ Example: | |||
"payer_name": "Nuran Verkleij", | |||
"order_id": "111", | |||
"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 | |||
@@ -58,11 +66,15 @@ More Details: | |||
from __future__ import unicode_literals | |||
import frappe | |||
import json | |||
import pytz | |||
from frappe import _ | |||
from frappe.utils import get_url, call_hook_method, cint | |||
from six.moves.urllib.parse import urlencode | |||
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 | |||
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", | |||
@@ -124,7 +136,7 @@ class PayPalSettings(Document): | |||
def get_payment_url(self, **kwargs): | |||
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: | |||
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], | |||
"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, amount, currency): | |||
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": 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")) | |||
if response.get("ACK")[0] != "Success": | |||
frappe.throw(_("Looks like something is wrong with this site's Paypal configuration.")) | |||
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) | |||
def get_express_checkout_details(token): | |||
try: | |||
@@ -181,14 +234,14 @@ def get_express_checkout_details(token): | |||
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") | |||
}, "Authorized", doc=doc) | |||
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: | |||
frappe.log_error(frappe.get_traceback()) | |||
@@ -196,19 +249,9 @@ def get_express_checkout_details(token): | |||
@frappe.whitelist(allow_guest=True, xss_safe=True) | |||
def confirm_payment(token): | |||
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({ | |||
"METHOD": "DoExpressCheckoutPayment", | |||
"PAYERID": data.get("payerid"), | |||
@@ -231,25 +274,148 @@ def confirm_payment(token): | |||
data.get("reference_docname")).run_method("on_payment_authorized", "Completed") | |||
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: | |||
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: | |||
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, | |||
"image_view": 0, | |||
"in_create": 1, | |||
"in_dialog": 0, | |||
"is_submittable": 0, | |||
"issingle": 1, | |||
"istable": 0, | |||
@@ -28,7 +28,15 @@ Example: | |||
"payer_name": "Nuran Verkleij", | |||
"order_id": "111", | |||
"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 | |||
@@ -58,8 +66,9 @@ from frappe import _ | |||
import json | |||
from six.moves.urllib.parse import urlencode | |||
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): | |||
supported_currencies = ["INR"] | |||
@@ -82,6 +91,89 @@ class RazorpaySettings(Document): | |||
if currency not in self.supported_currencies: | |||
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): | |||
integration_request = create_request_log(kwargs, "Host", "Razorpay") | |||
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.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: | |||
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_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: | |||
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) | |||
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?doctype={0}&docname={1}'.format(self.data.reference_doctype, self.data.reference_docname) | |||
else: | |||
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}) | |||
@@ -164,7 +275,7 @@ class RazorpaySettings(Document): | |||
"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({ | |||
"api_key": frappe.conf.sandbox_api_key, | |||
"api_secret": frappe.conf.sandbox_api_secret, | |||
@@ -172,6 +283,16 @@ class RazorpaySettings(Document): | |||
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): | |||
""" | |||
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.status = "Failed" | |||
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_import": 0, | |||
"allow_rename": 0, | |||
"autoname": "field:gateway_name", | |||
"beta": 0, | |||
"creation": "2017-03-09 17:18:29.458397", | |||
"custom": 0, | |||
@@ -13,6 +14,40 @@ | |||
"engine": "InnoDB", | |||
"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, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -24,7 +59,7 @@ | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_list_view": 1, | |||
"in_standard_filter": 0, | |||
"label": "Publishable Key", | |||
"length": 0, | |||
@@ -39,9 +74,43 @@ | |||
"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, | |||
"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, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -53,7 +122,7 @@ | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_list_view": 1, | |||
"in_standard_filter": 0, | |||
"label": "Secret Key", | |||
"length": 0, | |||
@@ -68,6 +137,133 @@ | |||
"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, | |||
"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 | |||
} | |||
], | |||
@@ -78,10 +274,10 @@ | |||
"image_view": 0, | |||
"in_create": 0, | |||
"is_submittable": 0, | |||
"issingle": 1, | |||
"issingle": 0, | |||
"istable": 0, | |||
"max_attachments": 0, | |||
"modified": "2017-03-09 17:19:25.087475", | |||
"modified": "2018-05-23 13:32:14.429916", | |||
"modified_by": "Administrator", | |||
"module": "Integrations", | |||
"name": "Stripe Settings", | |||
@@ -90,7 +286,6 @@ | |||
"permissions": [ | |||
{ | |||
"amend": 0, | |||
"apply_user_permissions": 0, | |||
"cancel": 0, | |||
"create": 1, | |||
"delete": 1, | |||
@@ -29,9 +29,9 @@ class StripeSettings(Document): | |||
'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: | |||
self.validate_stripe_credentails() | |||
@@ -55,50 +55,46 @@ class StripeSettings(Document): | |||
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() | |||
try: | |||
self.integration_request = create_request_log(self.data, "Host", "Stripe") | |||
return self.create_charge_on_stripe() | |||
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.")), | |||
"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): | |||
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: | |||
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.flags.status_changed_to = "Completed" | |||
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()) | |||
# 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.data.reference_doctype and self.data.reference_docname: | |||
@@ -112,7 +108,11 @@ class StripeSettings(Document): | |||
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' | |||
@@ -125,3 +125,8 @@ class StripeSettings(Document): | |||
"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") | |||
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, | |||
"fields": [ | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -24,7 +25,7 @@ | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_list_view": 1, | |||
"in_standard_filter": 0, | |||
"label": "Gateway", | |||
"length": 0, | |||
@@ -40,6 +41,68 @@ | |||
"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_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, | |||
@@ -52,7 +115,7 @@ | |||
"issingle": 0, | |||
"istable": 0, | |||
"max_attachments": 0, | |||
"modified": "2017-03-09 12:40:56.176464", | |||
"modified": "2018-02-05 14:24:33.526645", | |||
"modified_by": "Administrator", | |||
"module": "Core", | |||
"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 | |||
"name": "{{ title }}", | |||
"description": "{{ description }}", | |||
"subscription_id": "{{ subscription_id }}", | |||
"handler": function (response){ | |||
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 { | |||
background-color: #f5f7fa; | |||
} | |||
{% include "templates/styles/card_style.css" %} | |||
</style> | |||
{% endblock %} |
@@ -16,6 +16,7 @@ | |||
.hero-and-content { | |||
background-color: #f5f7fa; | |||
} | |||
{% include "templates/styles/card_style.css" %} | |||
</style> | |||
{% endblock %} |
@@ -8,13 +8,27 @@ | |||
<span class='indicator green'> | |||
{{ _("Success") }}</span> | |||
</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> | |||
<style> | |||
.hero-and-content { | |||
background-color: #f5f7fa; | |||
} | |||
{% include "templates/styles/card_style.css" %} | |||
</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 %} |
@@ -4,6 +4,13 @@ | |||
from __future__ import unicode_literals | |||
import frappe | |||
no_cache = True | |||
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['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'), | |||
_('Seems token you are using is invalid!'), | |||
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 header -%}{% endblock %} | |||
{%- block header -%} | |||
{% endblock %} | |||
{% 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> | |||
{% endblock %} | |||
{%- 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 | |||
import frappe | |||
from frappe import _ | |||
from frappe.utils import flt, cint | |||
from frappe.utils import cint, fmt_money | |||
import json | |||
from frappe.integrations.doctype.stripe_settings.stripe_settings import get_gateway_controller | |||
no_cache = 1 | |||
no_sitemap = 1 | |||
@@ -14,14 +15,23 @@ expected_keys = ('amount', 'title', 'description', 'reference_doctype', 'referen | |||
def get_context(context): | |||
context.no_cache = 1 | |||
context.publishable_key = get_api_key() | |||
# 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: | |||
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: | |||
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 | |||
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")): | |||
publishable_key = frappe.conf.sandbox_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) | |||
def make_payment(stripe_token_id, data, reference_doctype=None, reference_docname=None): | |||
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 | |||
}) | |||
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() | |||
return data | |||
return data |