From e7fb968b39472ea7e187f068cc800be72e088104 Mon Sep 17 00:00:00 2001 From: szaboat Date: Tue, 17 Jul 2012 16:32:17 +0200 Subject: [PATCH 1/3] Added subscription example --- .gitignore | 1 + config.py | 5 ++ tr_checkout/checkout.py | 11 ++- tr_subscription/README.md | 24 +++++ tr_subscription/payment_form.py | 88 +++++++++++++++++++ tr_subscription/static/styles.css | 13 +++ tr_subscription/subscription.py | 67 ++++++++++++++ .../templates/confirm_subscription.html | 20 +++++ tr_subscription/templates/layout.html | 11 +++ .../templates/new_subscription.html | 12 +++ 10 files changed, 249 insertions(+), 3 deletions(-) create mode 100644 config.py create mode 100644 tr_subscription/README.md create mode 100644 tr_subscription/payment_form.py create mode 100644 tr_subscription/static/styles.css create mode 100644 tr_subscription/subscription.py create mode 100644 tr_subscription/templates/confirm_subscription.html create mode 100644 tr_subscription/templates/layout.html create mode 100644 tr_subscription/templates/new_subscription.html 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..95c8f13 --- /dev/null +++ b/tr_subscription/payment_form.py @@ -0,0 +1,88 @@ +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.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..e3f5fde --- /dev/null +++ b/tr_subscription/subscription.py @@ -0,0 +1,67 @@ +# 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) + import pdb;pdb.set_trace() + 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(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..5d8619b --- /dev/null +++ b/tr_subscription/templates/confirm_subscription.html @@ -0,0 +1,20 @@ +$def with (subscription) +$var title:Confirm Subscription + +

Subscribe Result

+ +

Thank you for your subscription.

+ +

Subscription details:

+ +
+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() + +
From db29427fd79a7b5f50cd1ed4fc2548012f25e8b4 Mon Sep 17 00:00:00 2001 From: szaboat Date: Wed, 18 Jul 2012 10:55:00 +0200 Subject: [PATCH 2/3] Remove debugger. Add Cardholder name to form fields. --- tr_subscription/payment_form.py | 8 ++++++++ tr_subscription/subscription.py | 1 - 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/tr_subscription/payment_form.py b/tr_subscription/payment_form.py index 95c8f13..40e304f 100644 --- a/tr_subscription/payment_form.py +++ b/tr_subscription/payment_form.py @@ -60,6 +60,14 @@ def __build_form(self): 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.Hidden( "tr_data", value=braintree.Customer.tr_data_for_create({}, "http://localhost:8080/subscriptions/confirm/") diff --git a/tr_subscription/subscription.py b/tr_subscription/subscription.py index e3f5fde..8d3bd0f 100644 --- a/tr_subscription/subscription.py +++ b/tr_subscription/subscription.py @@ -43,7 +43,6 @@ def GET(self): # remove leading ? from query string query_string = web.ctx.query[1:] confirm_result = braintree.TransparentRedirect.confirm(query_string) - import pdb;pdb.set_trace() if confirm_result.is_success: token = confirm_result.customer.credit_cards[0].token subscription_result = braintree.Subscription.create({ From ac7450c99d8a8c471c93506cbb8c68636f27747b Mon Sep 17 00:00:00 2001 From: szaboat Date: Wed, 18 Jul 2012 15:08:26 +0200 Subject: [PATCH 3/3] More fields. --- tr_subscription/payment_form.py | 30 +++++++++++++++++-- tr_subscription/subscription.py | 2 +- .../templates/confirm_subscription.html | 29 ++++++++++++++++-- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/tr_subscription/payment_form.py b/tr_subscription/payment_form.py index 40e304f..33a192b 100644 --- a/tr_subscription/payment_form.py +++ b/tr_subscription/payment_form.py @@ -1,6 +1,7 @@ import web import braintree + class PaymentForm(object): def __init__(self, params={}, errors=braintree.ValidationErrorCollection()): self.params = params @@ -18,7 +19,7 @@ def __build_form(self): 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")) + class_=self.__error_class(self.errors.for_object("customer").for_object("customer").on("first_name")), ), web.form.Textbox( description="Last Name", @@ -66,7 +67,31 @@ def __build_form(self): 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")), + 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", @@ -93,4 +118,3 @@ def __get_nested_param(self, *keys): for key in keys[1:]: val = val and val.get(key) return val - diff --git a/tr_subscription/subscription.py b/tr_subscription/subscription.py index 8d3bd0f..c766383 100644 --- a/tr_subscription/subscription.py +++ b/tr_subscription/subscription.py @@ -50,7 +50,7 @@ def GET(self): "plan_id": "plan_1", }) if subscription_result.is_success: - return render.confirm_subscription(subscription_result.subscription) + 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(), diff --git a/tr_subscription/templates/confirm_subscription.html b/tr_subscription/templates/confirm_subscription.html index 5d8619b..6868937 100644 --- a/tr_subscription/templates/confirm_subscription.html +++ b/tr_subscription/templates/confirm_subscription.html @@ -1,13 +1,38 @@ -$def with (subscription) +$def with (customer, subscription) $var title:Confirm Subscription

Subscribe Result

Thank you for your subscription.

-

Subscription details:

+

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