diff --git a/vendor/registripe/LICENSE b/vendor/registripe/LICENSE new file mode 100644 index 00000000..8dada3ed --- /dev/null +++ b/vendor/registripe/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/registripe/__init__.py b/vendor/registripe/__init__.py new file mode 100644 index 00000000..607f7a49 --- /dev/null +++ b/vendor/registripe/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0-dev" diff --git a/vendor/registripe/apps.py b/vendor/registripe/apps.py new file mode 100644 index 00000000..c33f2b84 --- /dev/null +++ b/vendor/registripe/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class RegistripeConfig(AppConfig): + name = 'registripe' diff --git a/vendor/registripe/forms.py b/vendor/registripe/forms.py new file mode 100644 index 00000000..3816c5de --- /dev/null +++ b/vendor/registripe/forms.py @@ -0,0 +1,223 @@ +import copy +from registripe import models + +from django import forms +from django.core.urlresolvers import reverse +from django.db.models import F, Q +from django.forms import widgets +from django.utils import timezone + +from django_countries import countries +from django_countries.fields import LazyTypedChoiceField +from django_countries.widgets import CountrySelectWidget + + +class NoRenderWidget(forms.widgets.HiddenInput): + + def render(self, name, value, attrs=None): + return "" + + +def secure_striped(field): + ''' Calls stripe() with secure=True. ''' + return striped(field, True) + + +def striped(field, secure=False): + + oldwidget = field.widget + field.widget = StripeWidgetProxy(oldwidget, secure) + return field + + +class StripeWidgetProxy(widgets.Widget): + + def __init__(self, underlying, secure=False): + self.underlying = underlying + self.secure = secure + + def __deepcopy__(self, memo): + copy_underlying = copy.deepcopy(self.underlying, memo) + return type(self)(copy_underlying, self.secure) + + def __getattribute__(self, attr): + spr = super(StripeWidgetProxy, self).__getattribute__ + if attr in ("underlying", "render", "secure", "__deepcopy__"): + return spr(attr) + else: + return getattr(self.underlying, attr) + + def render(self, name, value, attrs=None): + + if not attrs: + attrs = {} + + attrs["data-stripe"] = name + + if self.secure: + name = "" + + return self.underlying.render(name, value, attrs=attrs) + + +class CreditCardForm(forms.Form): + + required_css_class = 'label-required' + + def _media(self): + js = ( + 'https://js.stripe.com/v2/', + reverse("registripe_pubkey"), + ) + + return forms.Media(js=js) + + media = property(_media) + + number = secure_striped(forms.CharField( + required=False, + label="Credit card Number", + help_text="Your credit card number, with or without spaces.", + max_length=255, + )) + exp_month = secure_striped(forms.IntegerField( + required=False, + label="Card expiry month", + min_value=1, + max_value=12, + )) + exp_year = secure_striped(forms.IntegerField( + required=False, + label="Card expiry year", + help_text="The expiry year for your card in 4-digit form", + min_value=timezone.now().year, + )) + cvc = secure_striped(forms.CharField( + required=False, + min_length=3, + max_length=4, + )) + + stripe_token = forms.CharField( + max_length=255, + # required=True, + widget=NoRenderWidget(), + ) + + name = striped(forms.CharField( + required=True, + label="Cardholder name", + help_text="The cardholder's name, as it appears on the credit card", + max_length=255, + )) + address_line1 = striped(forms.CharField( + required=True, + label="Cardholder account address, line 1", + max_length=255, + )) + address_line2 = striped(forms.CharField( + required=False, + label="Cardholder account address, line 2", + max_length=255, + )) + address_city = striped(forms.CharField( + required=True, + label="Cardholder account city", + max_length=255, + )) + address_state = striped(forms.CharField( + required=True, + max_length=255, + label="Cardholder account state or province", + )) + address_zip = striped(forms.CharField( + required=True, + max_length=255, + label="Cardholder account postal code", + )) + address_country = striped(LazyTypedChoiceField( + label="Cardholder account country", + choices=countries, + widget=CountrySelectWidget, + )) + + +class StripeRefundForm(forms.Form): + + required_css_class = 'label-required' + + def __init__(self, *args, **kwargs): + ''' + + Arguments: + user (User): The user whose charges we should filter to. + min_value (Decimal): The minimum value of the charges we should + show (currently, credit notes can only be cashed out in full.) + + ''' + user = kwargs.pop('user', None) + min_value = kwargs.pop('min_value', None) + super(StripeRefundForm, self).__init__(*args, **kwargs) + + payment_field = self.fields['payment'] + qs = payment_field.queryset + + if user: + qs = qs.filter( + charge__customer__user=user, + ) + + if min_value is not None: + # amount >= amount_to_refund + amount_refunded + # No refunds yet + q1 = ( + Q(charge__amount_refunded__isnull=True) & + Q(charge__amount__gte=min_value) + ) + # There are some refunds + q2 = ( + Q(charge__amount_refunded__isnull=False) & + Q(charge__amount__gte=( + F("charge__amount_refunded") + min_value)) + ) + qs = qs.filter(q1 | q2) + + payment_field.queryset = qs + + payment = forms.ModelChoiceField( + required=True, + queryset=models.StripePayment.objects.all(), + ) + + +'''{ +From stripe.js details: + +Card details: + +The first argument to createToken is a JavaScript object containing credit +card data entered by the user. It should contain the following required +members: + +number: card number as a string without any separators +(e.g., "4242424242424242") +exp_month: two digit number representing the card's expiration month +(e.g., 12) +exp_year: two or four digit number representing the card's expiration year +(e.g., 2017) +(The expiration date can also be passed as a single string.) + +cvc: optional, but we highly recommend you provide it to help prevent fraud. +This is the card's security code, as a string (e.g., "123"). +The following fields are entirely optional and cannot result in a token +creation failure: + +name: cardholder name +address_line1: billing address line 1 +address_line2: billing address line 2 +address_city: billing address city +address_state: billing address state +address_zip: billing postal code as a string (e.g., "94301") +address_country: billing address country +} +''' diff --git a/vendor/registripe/migrations/0001_initial.py b/vendor/registripe/migrations/0001_initial.py new file mode 100644 index 00000000..9fe1778c --- /dev/null +++ b/vendor/registripe/migrations/0001_initial.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-09-21 06:19 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('pinax_stripe', '0003_make_cvc_check_blankable'), + ('registrasion', '0005_auto_20160905_0945'), + ] + + operations = [ + migrations.CreateModel( + name='StripePayment', + fields=[ + ('paymentbase_ptr', models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, primary_key=True, serialize=False, + to='registrasion.PaymentBase')), + ('charge', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='pinax_stripe.Charge')), + ], + bases=('registrasion.paymentbase',), + ), + ] diff --git a/vendor/registripe/migrations/0002_stripecreditnoterefund.py b/vendor/registripe/migrations/0002_stripecreditnoterefund.py new file mode 100644 index 00000000..bfe8e17b --- /dev/null +++ b/vendor/registripe/migrations/0002_stripecreditnoterefund.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-09-23 06:36 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('pinax_stripe', '0003_make_cvc_check_blankable'), + ('registrasion', '0005_auto_20160905_0945'), + ('registripe', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='StripeCreditNoteRefund', + fields=[ + ('creditnoterefund_ptr', models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, primary_key=True, serialize=False, + to='registrasion.CreditNoteRefund')), + ('charge', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='pinax_stripe.Charge')), + ], + bases=('registrasion.creditnoterefund',), + ), + ] diff --git a/vendor/registripe/migrations/__init__.py b/vendor/registripe/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/vendor/registripe/models.py b/vendor/registripe/models.py new file mode 100644 index 00000000..be3aab6b --- /dev/null +++ b/vendor/registripe/models.py @@ -0,0 +1,15 @@ +from __future__ import unicode_literals + +from django.db import models +from registrasion.models import commerce +from pinax.stripe.models import Charge + + +class StripePayment(commerce.PaymentBase): + + charge = models.ForeignKey(Charge) + + +class StripeCreditNoteRefund(commerce.CreditNoteRefund): + + charge = models.ForeignKey(Charge) diff --git a/vendor/registripe/urls.py b/vendor/registripe/urls.py new file mode 100644 index 00000000..40e6b144 --- /dev/null +++ b/vendor/registripe/urls.py @@ -0,0 +1,15 @@ +from django.conf.urls import url + +from registripe import views + +from pinax.stripe.views import Webhook + + +urlpatterns = [ + url(r"^card/([0-9]*)/$", views.card, name="registripe_card"), + url(r"^card/([0-9]*)/([0-9A-Za-z]*)/$", views.card, + name="registripe_card"), + url(r"^pubkey/$", views.pubkey_script, name="registripe_pubkey"), + url(r"^refund/([0-9]*)/$", views.refund, name="registripe_refund"), + url(r"^webhook/$", Webhook.as_view(), name="pinax_stripe_webhook"), +] diff --git a/vendor/registripe/views.py b/vendor/registripe/views.py new file mode 100644 index 00000000..daf685d0 --- /dev/null +++ b/vendor/registripe/views.py @@ -0,0 +1,197 @@ +from registripe import forms +from registripe import models + +from django.core.exceptions import ValidationError +from django.conf import settings +from django.contrib import messages +from django.contrib.auth.decorators import user_passes_test +from django.db import transaction +from django.http import Http404 +from django.http import HttpResponse +from django.shortcuts import redirect, render + +from registrasion.controllers.credit_note import CreditNoteController +from registrasion.controllers.invoice import InvoiceController + +from pinax.stripe import actions + +from stripe.error import StripeError + +from symposion.conference.models import Conference + +CURRENCY = settings.INVOICE_CURRENCY +CONFERENCE_ID = settings.CONFERENCE_ID + + +def _staff_only(user): + ''' Returns true if the user is staff. ''' + return user.is_staff + + +def pubkey_script(request): + ''' Returns a JS snippet that sets the Stripe public key for Stripe.js. ''' + + script_template = "Stripe.setPublishableKey('%s');" + script = script_template % settings.PINAX_STRIPE_PUBLIC_KEY + + return HttpResponse(script, content_type="text/javascript") + + +def card(request, invoice_id, access_code=None): + ''' View that shows and processes a Stripe CreditCardForm to pay the given + invoice. Redirects back to the invoice once the invoice is fully paid. + + Arguments: + invoice_id (castable to str): The invoice id for the invoice to pay. + access_code (str): The optional access code for the invoice (for + unauthenticated payment) + + ''' + + form = forms.CreditCardForm(request.POST or None) + + inv = InvoiceController.for_id_or_404(str(invoice_id)) + + if not inv.can_view(user=request.user, access_code=access_code): + raise Http404() + + args = [inv.invoice.id] + if access_code: + args.append(access_code) + to_invoice = redirect("invoice", *args) + + if inv.invoice.balance_due() <= 0: + return to_invoice + + if request.POST and form.is_valid(): + try: + inv.validate_allowed_to_pay() + process_card(request, form, inv) + return to_invoice + except StripeError as e: + form.add_error(None, ValidationError(e)) + except ValidationError as ve: + form.add_error(None, ve) + + data = { + "invoice": inv.invoice, + "form": form, + } + + return render( + request, "registrasion/stripe/credit_card_payment.html", data + ) + + +@transaction.atomic +def process_card(request, form, inv): + ''' Processes the given credit card form + + Arguments: + request: the current request context + form: a CreditCardForm + inv: an InvoiceController + ''' + + conference = Conference.objects.get(id=CONFERENCE_ID) + amount_to_pay = inv.invoice.balance_due() + user = inv.invoice.user + token = form.cleaned_data["stripe_token"] + + customer = actions.customers.get_customer_for_user(user) + + if not customer: + customer = actions.customers.create(user) + + card = actions.sources.create_card(customer, token) + + description = "Payment for %s invoice #%s" % ( + conference.title, inv.invoice.id + ) + + charge = actions.charges.create( + amount_to_pay, + customer, + source=card, + currency=CURRENCY, + description=description, + capture=True, + ) + + receipt = charge.stripe_charge.id + reference = "Paid with Stripe reference: " + receipt + + # Create the payment object + models.StripePayment.objects.create( + invoice=inv.invoice, + reference=reference, + amount=charge.amount, + charge=charge, + ) + + inv.update_status() + + messages.success(request, "This invoice was successfully paid.") + + +@user_passes_test(_staff_only) +def refund(request, credit_note_id): + ''' Allows staff to select a Stripe charge for the owner of the credit + note, and refund the credit note into stripe. ''' + + cn = CreditNoteController.for_id_or_404(str(credit_note_id)) + + to_credit_note = redirect("credit_note", cn.credit_note.id) + + if not cn.credit_note.is_unclaimed: + return to_credit_note + + form = forms.StripeRefundForm( + request.POST or None, + user=cn.credit_note.invoice.user, + min_value=cn.credit_note.value, + ) + + if request.POST and form.is_valid(): + try: + process_refund(cn, form) + return to_credit_note + except StripeError as se: + form.add_error(None, ValidationError(se)) + + data = { + "credit_note": cn.credit_note, + "form": form, + } + + return render( + request, "registrasion/stripe/refund.html", data + ) + + +def process_refund(cn, form): + raise NotImplementedError("Does not actually refund a user") + payment = form.cleaned_data["payment"] + charge = payment.charge + + to_refund = cn.credit_note.value + stripe_charge_id = charge.stripe_charge.id + + # Test that the given charge is allowed to be refunded. + max_refund = actions.charges.calculate_refund_amount(charge) + + if max_refund < to_refund: + raise ValidationError( + "You must select a payment holding greater value than " + "the credit note." + ) + + refund = actions.refunds.create(charge, to_refund) # noqa + + models.StripeCreditNoteRefund.objects.create( + parent=cn.credit_note, + charge=charge, + reference="Refunded %s to Stripe charge %s" % ( + to_refund, stripe_charge_id + ) + )