diff --git a/.gitignore b/.gitignore index 0d20b64..13f461a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ +config.py *.pyc diff --git a/config.py b/config.py new file mode 100644 index 0000000..2665df5 --- /dev/null +++ b/config.py @@ -0,0 +1,5 @@ +config = { + 'BRAINTREE_MERCHANT_ID':"your_merchant_id", + 'BRAINTREE_PUBLIC_KEY':"your_public_key", + 'BRAINTREE_PRIVATE_KEY':"your_private_key", +} \ No newline at end of file diff --git a/tr_checkout/checkout.py b/tr_checkout/checkout.py index c1cd837..40124cb 100644 --- a/tr_checkout/checkout.py +++ b/tr_checkout/checkout.py @@ -3,15 +3,20 @@ import urllib2 urllib2.install_opener(urllib2.build_opener()) +import os +parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +os.sys.path.insert(0,parentdir) +from config import config + import braintree import web import payment_form braintree.Configuration.configure( braintree.Environment.Sandbox, - "your_merchant_id", - "your_public_key", - "your_private_key" + config['BRAINTREE_MERCHANT_ID'], + config['BRAINTREE_PUBLIC_KEY'], + config['BRAINTREE_PRIVATE_KEY'], ) urls = ( diff --git a/tr_subscription/README.md b/tr_subscription/README.md new file mode 100644 index 0000000..aceebb3 --- /dev/null +++ b/tr_subscription/README.md @@ -0,0 +1,24 @@ +# tr_subscription + +## Overview + +This code is a demonstration of creating one time transactions using the +Braintree transparent redirect API. We decided to use the +[web.py](http://webpy.org/) framework because its simplicity +allows us to clearly demonstrate concepts without added noise. + +Assuming you have a plan with the following details: + + plan_id: "plan_1" + +## Getting started + +* pip install braintree +* pip install web.py +* Update config.py with your merchant_id, public_key and private_key +* cd tr_subscription +* python subscription.py +* Visit [http://localhost:8080/payments/new](http://localhost:8080/payments/new) + + + diff --git a/tr_subscription/payment_form.py b/tr_subscription/payment_form.py new file mode 100644 index 0000000..33a192b --- /dev/null +++ b/tr_subscription/payment_form.py @@ -0,0 +1,120 @@ +import web +import braintree + + +class PaymentForm(object): + def __init__(self, params={}, errors=braintree.ValidationErrorCollection()): + self.params = params + self.errors = errors + + def form(self): + form = self.__build_form() + return form() + + def __build_form(self): + return web.form.Form( + web.form.Textbox( + description="First Name", + name="customer[first_name]", + id="customer_customer_first_name", + value=self.__get_nested_param("customer", "first_name"), + post=self.__error_message(self.errors.for_object("customer").for_object("customer").on("first_name")), + class_=self.__error_class(self.errors.for_object("customer").for_object("customer").on("first_name")), + ), + web.form.Textbox( + description="Last Name", + name="customer[last_name]", + id="customer_customer_last_name", + value=self.__get_nested_param("customer", "last_name"), + post=self.__error_message(self.errors.for_object("customer").for_object("customer").on("last_name")), + class_=self.__error_class(self.errors.for_object("customer").for_object("customer").on("last_name")) + ), + web.form.Textbox( + description="Email", + name="customer[email]", + id="customer_customer_email", + value=self.__get_nested_param("customer", "email"), + post=self.__error_message(self.errors.for_object("customer").for_object("customer").on("email")), + class_=self.__error_class(self.errors.for_object("customer").for_object("customer").on("email")) + ), + web.form.Textbox( + description="Number", + name="customer[credit_card][number]", + id="customer_credit_card_number", + value=self.__get_nested_param("customer", "credit_card", "number"), + post=self.__error_message(self.errors.for_object("customer").for_object("credit_card").on("number")), + class_=self.__error_class(self.errors.for_object("customer").for_object("credit_card").on("number")) + ), + web.form.Textbox( + description="Expiration Date", + name="customer[credit_card][expiration_date]", + id="customer_credit_card_exipration_date", + value=self.__get_nested_param("customer", "credit_card", "expiration_date"), + post=self.__error_message(self.errors.for_object("customer").for_object("credit_card").on("expiration_date")), + class_=self.__error_class(self.errors.for_object("customer").for_object("credit_card").on("expiration_date")) + ), + web.form.Textbox( + description="CVV", + name="customer[credit_card][cvv]", + id="customer_credit_card_cvv", + value=self.__get_nested_param("customer", "credit_card", "cvv"), + post=self.__error_message(self.errors.for_object("customer").for_object("credit_card").on("cvv")), + class_=self.__error_class(self.errors.for_object("customer").for_object("credit_card").on("cvv")) + ), + web.form.Textbox( + description="Cardholder name", + name="customer[credit_card][cardholder_name]", + id="customer_credit_card_cardholder_name", + value=self.__get_nested_param("customer", "credit_card", "cardholder_name"), + post=self.__error_message(self.errors.for_object("customer").for_object("credit_card").on("cardholder_name")), + class_=self.__error_class(self.errors.for_object("customer").for_object("credit_card").on("cardholder_name")) + ), + web.form.Textbox( + description="Street", + name="customer[credit_card][billing_address][street_address]", + id="customer_credit_card_billing_address_street_address", + value=self.__get_nested_param("customer", "credit_card", "billing_address", "street_address"), + post=self.__error_message(self.errors.for_object("customer").for_object("credit_card").for_object("billing_address").on("street_address")), + class_=self.__error_class(self.errors.for_object("customer").for_object("credit_card").for_object("billing_address").on("street_address")) + ), + web.form.Textbox( + description="Country code", + name="customer[credit_card][billing_address][country_code_alpha2]", + id="customer_credit_card_billing_address_country_code_alpha2", + value=self.__get_nested_param("customer", "credit_card", "billing_address", "country_code_alpha2"), + post=self.__error_message(self.errors.for_object("customer").for_object("credit_card").for_object("billing_address").on("country_code_alpha2")), + class_=self.__error_class(self.errors.for_object("customer").for_object("credit_card").for_object("billing_address").on("country_code_alpha2")) + ), + web.form.Textbox( + description="Postal Code", + name="customer[credit_card][billing_address][postal_code]", + id="customer_credit_card_billing_address_postal_code", + value=self.__get_nested_param("customer", "credit_card", "billing_address", "postal_code"), + post=self.__error_message(self.errors.for_object("customer").for_object("credit_card").for_object("billing_address").on("postal_code")), + class_=self.__error_class(self.errors.for_object("customer").for_object("credit_card").for_object("billing_address").on("postal_code")) + ), + web.form.Hidden( + "tr_data", + value=braintree.Customer.tr_data_for_create({}, "http://localhost:8080/subscriptions/confirm/") + ) + ) + + def __error_class(self, errors): + if len(errors) == 0: + return "" + else: + return "error" + + def __error_message(self, errors): + if len(errors) == 0: + return "" + else: + message = "
" + message += " ".join([error.message for error in errors]) + return message + "
" + + def __get_nested_param(self, *keys): + val = self.params.get(keys[0]) + for key in keys[1:]: + val = val and val.get(key) + return val diff --git a/tr_subscription/static/styles.css b/tr_subscription/static/styles.css new file mode 100644 index 0000000..06702a3 --- /dev/null +++ b/tr_subscription/static/styles.css @@ -0,0 +1,13 @@ +input.error { + border: 2px solid red; +} + +div.errors { + display: inline; + margin-left: 0.5em; + color: red; +} + +h3#error-count { + color: red; +} diff --git a/tr_subscription/subscription.py b/tr_subscription/subscription.py new file mode 100644 index 0000000..c766383 --- /dev/null +++ b/tr_subscription/subscription.py @@ -0,0 +1,66 @@ +# these requires are necessary on OSX 10.6.x to prevent +# thread issues +import urllib2 +urllib2.install_opener(urllib2.build_opener()) + +import os +parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +os.sys.path.insert(0, parentdir) +from config import config + +import braintree +import web +import payment_form + + +braintree.Configuration.configure( + braintree.Environment.Sandbox, + config['BRAINTREE_MERCHANT_ID'], + config['BRAINTREE_PUBLIC_KEY'], + config['BRAINTREE_PRIVATE_KEY'], +) + +urls = ( + "/", "Subscription", + "/subscriptions/new", "Subscription", + "/subscriptions/confirm/", "Confirm", +) + +app = web.application(urls, globals()) +render = web.template.render("templates", base="layout") + + +class Subscription: + def GET(self): + return render.new_subscription( + payment_form.PaymentForm().form(), + 0, + braintree.TransparentRedirect.url()) + + +class Confirm: + def GET(self): + # remove leading ? from query string + query_string = web.ctx.query[1:] + confirm_result = braintree.TransparentRedirect.confirm(query_string) + if confirm_result.is_success: + token = confirm_result.customer.credit_cards[0].token + subscription_result = braintree.Subscription.create({ + "payment_method_token": token, + "plan_id": "plan_1", + }) + if subscription_result.is_success: + return render.confirm_subscription(confirm_result.customer, subscription_result.subscription) + else: + return render.new_subscription( + payment_form.PaymentForm(confirm_result.params, confirm_result.errors).form(), + len(confirm_result.errors), + braintree.TransparentRedirect.url()) + else: + return render.new_subscription( + payment_form.PaymentForm(confirm_result.params, confirm_result.errors).form(), + len(confirm_result.errors), + braintree.TransparentRedirect.url()) + +if __name__ == "__main__": + app.run() diff --git a/tr_subscription/templates/confirm_subscription.html b/tr_subscription/templates/confirm_subscription.html new file mode 100644 index 0000000..6868937 --- /dev/null +++ b/tr_subscription/templates/confirm_subscription.html @@ -0,0 +1,45 @@ +$def with (customer, subscription) +$var title:Confirm Subscription + +

Subscribe Result

+ +

Thank you for your subscription.

+ +

Details:

+ +
+# Customer
+
+customer.first_name $customer.first_name
+customer.last_name $customer.last_name
+customer.email $customer.email
+
+# Addresses
+
+first_name,
+last_name,
+country_code_alpha2,
+country_code_alpha3,
+extended_address,
+locality,
+country_code_numeric,
+postal_code,
+gateway,
+country_name,
+region,
+customer_id,
+company,
+street_address
+
+# Subscription
+
+subscription.balance $subscription.balance
+subscription.merchant_account_id $subscription.merchant_account_id
+subscription.payment_method_token $subscription.payment_method_token
+subscription.plan_id $subscription.plan_id
+subscription.price $subscription.price
+subscription.status $subscription.status
+subscription.trial_duration $subscription.trial_duration
+subscription.trial_duration_unit $subscription.trial_duration_unit
+subscription.trial_period $subscription.trial_period
+
\ No newline at end of file diff --git a/tr_subscription/templates/layout.html b/tr_subscription/templates/layout.html new file mode 100644 index 0000000..5b7cea9 --- /dev/null +++ b/tr_subscription/templates/layout.html @@ -0,0 +1,11 @@ +$def with (page) + + + + $page.title + + + + $:page + + diff --git a/tr_subscription/templates/new_subscription.html b/tr_subscription/templates/new_subscription.html new file mode 100644 index 0000000..52f9336 --- /dev/null +++ b/tr_subscription/templates/new_subscription.html @@ -0,0 +1,12 @@ +$def with (form, error_count, url) +$var title:New Payment + +

Subscribe to plan_1

+ +$if error_count > 0: +

$error_count error(s)

+ +
+ $:form.render() + +