diff --git a/datacenterlight/locale/de/LC_MESSAGES/django.po b/datacenterlight/locale/de/LC_MESSAGES/django.po index 4a95c2fc..1b66b640 100644 --- a/datacenterlight/locale/de/LC_MESSAGES/django.po +++ b/datacenterlight/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-05-12 21:43+0530\n" +"POT-Creation-Date: 2018-07-05 23:11+0000\n" "PO-Revision-Date: 2018-03-30 23:22+0000\n" "Last-Translator: b'Anonymous User '\n" "Language-Team: LANGUAGE \n" @@ -329,6 +329,17 @@ msgstr "wird an der Kasse angewendet" msgid "Credit Card" msgstr "Kreditkarte" +msgid "" +"Please select one of the cards that you used before or fill in your credit " +"card information below. We are using Stripe for payment and do not store your information in our " +"database." +msgstr "" +"Bitte wähle eine der zuvor genutzten Kreditkarten oder gib Deine " +"Kreditkartendetails unten an. Die Bezahlung wird über " +"Stripe abgewickelt. " +"Wir speichern Deine Kreditkartendetails nicht in unserer Datenbank." + msgid "" "Please fill in your credit card information below. We are using Stripe for payment and do not " @@ -338,31 +349,23 @@ msgstr "" "\"https://stripe.com\" target=\"_blank\">Stripe für die Bezahlung und " "speichern keine Informationen in unserer Datenbank." -msgid "" -"You are not making any payment yet. After submitting your card information, " -"you will be taken to the Confirm Order Page." -msgstr "" -"Es wird noch keine Bezahlung vorgenommen. Die Bezahlung wird erst ausgelöst, " -"nachdem Du die Bestellung auf der nächsten Seite bestätigt hast." +msgid "Last" +msgstr "Letzten" -msgid "Card Number" -msgstr "Kreditkartennummer" +msgid "Type" +msgstr "Typ" -msgid "Expiry Date" -msgstr "Ablaufdatum" +msgid "SELECT" +msgstr "AUSWÄHLEN" -msgid "CVC" -msgstr "" +msgid "Add a new credit card" +msgstr "Eine neue Kreditkarte hinzufügen" -msgid "Card Type" -msgstr "Kartentyp" +msgid "NEW CARD" +msgstr "NEUE KARTE" -msgid "" -"You are not making any payment yet. After placing your order, you will be " -"taken to the Submit Payment Page." -msgstr "" -"Es wird noch keine Bezahlung vorgenommen. Die Bezahlung wird erst ausgelöst, " -"nachdem Du die Bestellung auf der nächsten Seite bestätigt hast." +msgid "New Credit Card" +msgstr "Neue Kreditkarte" msgid "Processing" msgstr "Weiter" @@ -516,6 +519,13 @@ msgstr "Ungültige Speicher-Grösse" msgid "Incorrect pricing name. Please contact support{support_email}" msgstr "" +#, python-brace-format +msgid "{user} does not have permission to access the card" +msgstr "{user} hat keine Erlaubnis auf diese Karte zuzugreifen" + +msgid "An error occurred. Details: {}" +msgstr "Ein Fehler ist aufgetreten. Details: {}" + msgid "Confirm Order" msgstr "Bestellung Bestätigen" @@ -529,6 +539,11 @@ msgstr "" "Es ist ein Fehler bei der Zahlung betreten. Du wirst nach dem Schliessen vom " "Popup zur Bezahlseite weitergeleitet." +#, python-brace-format +msgid "An error occurred while associating the card. Details: {details}" +msgstr "Beim Verbinden der Karte ist ein Fehler aufgetreten. Details: " +"{details}" + msgid "Thank you for the order." msgstr "Danke für Deine Bestellung." @@ -539,6 +554,28 @@ msgstr "" "Deine VM ist gleich bereit. Wir senden Dir eine Bestätigungsemail, sobald Du " "auf sie zugreifen kannst." +#~ msgid "" +#~ "You are not making any payment yet. After submitting your card " +#~ "information, you will be taken to the Confirm Order Page." +#~ msgstr "" +#~ "Es wird noch keine Bezahlung vorgenommen. Die Bezahlung wird erst " +#~ "ausgelöst, nachdem Du die Bestellung auf der nächsten Seite bestätigt " +#~ "hast." + +#~ msgid "Card Number" +#~ msgstr "Kreditkartennummer" + +#~ msgid "Expiry Date" +#~ msgstr "Ablaufdatum" + +#~ msgid "" +#~ "You are not making any payment yet. After placing your order, you will be " +#~ "taken to the Submit Payment Page." +#~ msgstr "" +#~ "Es wird noch keine Bezahlung vorgenommen. Die Bezahlung wird erst " +#~ "ausgelöst, nachdem Du die Bestellung auf der nächsten Seite bestätigt " +#~ "hast." + #~ msgid "Pricing" #~ msgstr "Preise" diff --git a/datacenterlight/static/datacenterlight/css/common.css b/datacenterlight/static/datacenterlight/css/common.css index b6eabd75..28674b30 100644 --- a/datacenterlight/static/datacenterlight/css/common.css +++ b/datacenterlight/static/datacenterlight/css/common.css @@ -158,4 +158,25 @@ footer .dcl-link-separator::before { .thin-hr { margin-top: 10px; margin-bottom: 10px; +} + +.payment-container .credit-card-info { + padding-bottom: 15px; + border-bottom: 1px solid #eee; +} +.credit-card-info { + display: flex; +} + +.credit-card-info .align-bottom { + align-self: flex-end; + padding-right: 0 !important; +} + +.new-card-head { + margin-top: 10px; +} +.new-card-button-margin button{ + margin-top: 5px; + margin-bottom: 5px; } \ No newline at end of file diff --git a/datacenterlight/templates/datacenterlight/landing_payment.html b/datacenterlight/templates/datacenterlight/landing_payment.html index 4d111fa1..4c43f41c 100644 --- a/datacenterlight/templates/datacenterlight/landing_payment.html +++ b/datacenterlight/templates/datacenterlight/landing_payment.html @@ -101,93 +101,55 @@
+ {% with card_list_len=cards_list|length %}

{%trans "Credit Card"%}


- {% blocktrans %}Please fill in your credit card information below. We are using Stripe for payment and do not store your information in our database.{% endblocktrans %} -

-
- {% if credit_card_data.last4 %} -
-
Credit Card
-
Last 4: *****{{credit_card_data.last4}}
-
Type: {{credit_card_data.cc_brand}}
- -
- {% if not messages and not form.non_field_errors %} -

- {% trans "You are not making any payment yet. After submitting your card information, you will be taken to the Confirm Order Page." %} -

- {% endif %} -
- {% for message in messages %} - {% if 'failed_payment' or 'make_charge_error' in message.tags %} -
    -
  • -

    {{ message|safe }}

    -
  • -
- {% endif %} - {% endfor %} - {% for error in form.non_field_errors %} -

- {{ error|escape }} -

- {% endfor %} -
-
- -
+ {% if card_list_len > 0 %} + {% blocktrans %}Please select one of the cards that you used before or fill in your credit card information below. We are using Stripe for payment and do not store your information in our database.{% endblocktrans %} {% else %} -
- -
-
-
- -
-
-
-
- -
-
-
- -
-
-
-
- - -
+ {% blocktrans %}Please fill in your credit card information below. We are using Stripe for payment and do not store your information in our database.{% endblocktrans %} + {% endif %} +

+
+ {% for card in cards_list %} +
+
+
{% trans "Credit Card" %}
+
{% trans "Last" %} 4: ***** {{card.last4}}
+
{% trans "Type" %}: {{card.brand}}
+
+
-
- {% if not messages and not form.non_field_errors %} -

- {% trans "You are not making any payment yet. After placing your order, you will be taken to the Submit Payment Page." %} -

- {% endif %} -
- {% for message in messages %} - {% if 'failed_payment' in message.tags or 'make_charge_error' in message.tags or 'error' in message.tags %} -
    -
  • {{ message|safe }}

  • -
- {% endif %} - {% endfor %} + {% endfor %} + {% if card_list_len > 0 %} +
+
+
+

{% trans "Add a new credit card" %}

+
+
+ +
+
-
- +
+
+
+

{%trans "New Credit Card" %}

+
+ {% include "hosting/includes/_card_input.html" %} +
- -
-

-
- - {% endif %} -
+ {% else%} + {% include "hosting/includes/_card_input.html" %} + {% endif %} +
+ {% endwith %}
@@ -207,13 +169,4 @@ })(); {%endif%} - -{% if credit_card_data.last4 and credit_card_data.cc_brand %} - -{%endif%} - {%endblock%} diff --git a/datacenterlight/tests.py b/datacenterlight/tests.py index d6cd6adf..c755cc6f 100644 --- a/datacenterlight/tests.py +++ b/datacenterlight/tests.py @@ -86,8 +86,7 @@ class CeleryTaskTestCase(TestCase): token=self.token ) card_details = self.stripe_utils.get_card_details( - stripe_customer.stripe_id, - self.token + stripe_customer.stripe_id ) card_details_dict = card_details.get('error') self.assertEquals(card_details_dict, None) diff --git a/datacenterlight/utils.py b/datacenterlight/utils.py index a6f760af..1c54148e 100644 --- a/datacenterlight/utils.py +++ b/datacenterlight/utils.py @@ -87,7 +87,7 @@ def create_vm(billing_address_data, stripe_customer_id, specs, create_vm_task.delay(vm_template_id, user, specs, template, order.id) for session_var in ['specs', 'template', 'billing_address', - 'billing_address_data', + 'billing_address_data', 'card_id', 'token', 'customer']: if session_var in request.session: del request.session[session_var] diff --git a/datacenterlight/views.py b/datacenterlight/views.py index db36d23a..bf87d9b9 100644 --- a/datacenterlight/views.py +++ b/datacenterlight/views.py @@ -13,7 +13,7 @@ from django.views.decorators.cache import cache_control from django.views.generic import FormView, CreateView, DetailView from hosting.forms import HostingUserLoginForm -from hosting.models import HostingOrder +from hosting.models import HostingOrder, UserCardDetail from membership.models import CustomUser, StripeCustomer from opennebula_api.serializers import VMTemplateSerializer from utils.forms import BillingAddressForm, BillingAddressFormSignup @@ -222,19 +222,15 @@ class PaymentOrderView(FormView): billing_address_form = BillingAddressForm( instance=self.request.user.billing_addresses.first() ) - # Get user last order - last_hosting_order = HostingOrder.objects.filter( - customer__user=self.request.user - ).last() - - # If user has already an hosting order, get the credit card - # data from it - if last_hosting_order: - credit_card_data = last_hosting_order.get_cc_data() - if credit_card_data: - context['credit_card_data'] = credit_card_data - else: - context['credit_card_data'] = None + user = self.request.user + if hasattr(user, 'stripecustomer'): + stripe_customer = user.stripecustomer + else: + stripe_customer = None + cards_list = UserCardDetail.get_all_cards_list( + stripe_customer=stripe_customer + ) + context.update({'cards_list': cards_list}) else: billing_address_form = BillingAddressFormSignup( initial=billing_address_data @@ -286,14 +282,42 @@ class PaymentOrderView(FormView): ) if address_form.is_valid(): token = address_form.cleaned_data.get('token') + if token is '': + card_id = address_form.cleaned_data.get('card') + try: + user_card_detail = UserCardDetail.objects.get(id=card_id) + if not request.user.has_perm( + 'view_usercarddetail', user_card_detail + ): + raise UserCardDetail.DoesNotExist( + _("{user} does not have permission to access the " + "card").format(user=request.user.email) + ) + except UserCardDetail.DoesNotExist as e: + ex = str(e) + logger.error("Card Id: {card_id}, Exception: {ex}".format( + card_id=card_id, ex=ex + ) + ) + msg = _("An error occurred. Details: {}".format(ex)) + messages.add_message( + self.request, messages.ERROR, msg, + extra_tags='make_charge_error' + ) + return HttpResponseRedirect( + reverse('datacenterlight:payment') + '#payment_error' + ) + request.session['card_id'] = user_card_detail.id + else: + request.session['token'] = token if request.user.is_authenticated(): this_user = { 'email': request.user.email, 'name': request.user.name } customer = StripeCustomer.get_or_create( - email=this_user.get('email'), - token=token) + email=this_user.get('email'), token=token + ) else: user_email = address_form.cleaned_data.get('email') user_name = address_form.cleaned_data.get('name') @@ -341,7 +365,6 @@ class PaymentOrderView(FormView): billing_address_form=address_form ) ) - request.session['token'] = token if type(customer) is StripeCustomer: request.session['customer'] = customer.stripe_id else: @@ -362,32 +385,34 @@ class OrderConfirmationView(DetailView): @cache_control(no_cache=True, must_revalidate=True, no_store=True) def get(self, request, *args, **kwargs): + context = {} if 'specs' not in request.session or 'user' not in request.session: return HttpResponseRedirect(reverse('datacenterlight:index')) - if 'token' not in request.session: - return HttpResponseRedirect(reverse('datacenterlight:payment')) - stripe_api_cus_id = request.session.get('customer') - stripe_utils = StripeUtils() - card_details = stripe_utils.get_card_details(stripe_api_cus_id, - request.session.get( - 'token')) - if not card_details.get('response_object'): - msg = card_details.get('error') - messages.add_message(self.request, messages.ERROR, msg, - extra_tags='failed_payment') - return HttpResponseRedirect( - reverse('datacenterlight:payment') + '#payment_error') - context = { + if 'token' in self.request.session: + token = self.request.session['token'] + stripe_utils = StripeUtils() + card_details = stripe_utils.get_cards_details_from_token( + token + ) + if not card_details.get('response_object'): + return HttpResponseRedirect(reverse('hosting:payment')) + card_details_response = card_details['response_object'] + context['cc_last4'] = card_details_response['last4'] + context['cc_brand'] = card_details_response['brand'] + else: + card_id = self.request.session.get('card_id') + card_detail = UserCardDetail.objects.get(id=card_id) + context['cc_last4'] = card_detail.last4 + context['cc_brand'] = card_detail.brand + context.update({ 'site_url': reverse('datacenterlight:index'), - 'cc_last4': card_details.get('response_object').get('last4'), - 'cc_brand': card_details.get('response_object').get('brand'), 'vm': request.session.get('specs'), 'page_header_text': _('Confirm Order'), 'billing_address_data': ( request.session.get('billing_address_data') ), 'cms_integration': get_cms_integration('default'), - } + }) return render(request, self.template_name, context) def post(self, request, *args, **kwargs): @@ -397,13 +422,75 @@ class OrderConfirmationView(DetailView): stripe_api_cus_id = request.session.get('customer') vm_template_id = template.get('id', 1) stripe_utils = StripeUtils() - card_details = stripe_utils.get_card_details(stripe_api_cus_id, - request.session.get( - 'token')) - if not card_details.get('response_object'): - msg = card_details.get('error') - messages.add_message(self.request, messages.ERROR, msg, - extra_tags='failed_payment') + + if 'token' in request.session: + card_details = stripe_utils.get_cards_details_from_token( + request.session.get('token') + ) + if not card_details.get('response_object'): + msg = card_details.get('error') + messages.add_message(self.request, messages.ERROR, msg, + extra_tags='failed_payment') + response = { + 'status': False, + 'redirect': "{url}#{section}".format( + url=reverse('datacenterlight:payment'), + section='payment_error'), + 'msg_title': str(_('Error.')), + 'msg_body': str( + _('There was a payment related error.' + ' On close of this popup, you will be' + ' redirected back to the payment page.') + ) + } + return JsonResponse(response) + card_details_response = card_details['response_object'] + card_details_dict = { + 'last4': card_details_response['last4'], + 'brand': card_details_response['brand'], + 'card_id': card_details_response['card_id'] + } + stripe_customer_obj = StripeCustomer.objects.filter(stripe_id=stripe_api_cus_id).first() + if stripe_customer_obj: + ucd = UserCardDetail.get_user_card_details( + stripe_customer_obj, card_details_response + ) + if not ucd: + acc_result = stripe_utils.associate_customer_card( + stripe_api_cus_id, request.session['token'], + set_as_default=True + ) + if acc_result['response_object'] is None: + msg = _( + 'An error occurred while associating the card.' + ' Details: {details}'.format( + details=acc_result['error'] + ) + ) + messages.add_message(self.request, messages.ERROR, msg, + extra_tags='failed_payment') + response = { + 'status': False, + 'redirect': "{url}#{section}".format( + url=reverse('hosting:payment'), + section='payment_error'), + 'msg_title': str(_('Error.')), + 'msg_body': str( + _('There was a payment related error.' + ' On close of this popup, you will be redirected' + ' back to the payment page.') + ) + } + return JsonResponse(response) + elif 'card_id' in request.session: + card_id = request.session.get('card_id') + user_card_detail = UserCardDetail.objects.get(id=card_id) + card_details_dict = { + 'last4': user_card_detail.last4, + 'brand': user_card_detail.brand, + 'card_id': user_card_detail.card_id + } + else: response = { 'status': False, 'redirect': "{url}#{section}".format( @@ -417,7 +504,6 @@ class OrderConfirmationView(DetailView): } return JsonResponse(response) - card_details_dict = card_details.get('response_object') cpu = specs.get('cpu') memory = specs.get('memory') disk_size = specs.get('disk_size') @@ -442,6 +528,12 @@ class OrderConfirmationView(DetailView): # Check if the subscription was approved and is active if (stripe_subscription_obj is None or stripe_subscription_obj.status != 'active'): + # At this point, we have created a Stripe API card and + # associated it with the customer; but the transaction failed + # due to some reason. So, we would want to dissociate this card + # here. + # ... + msg = subscription_result.get('error') messages.add_message(self.request, messages.ERROR, msg, extra_tags='failed_payment') @@ -496,12 +588,36 @@ class OrderConfirmationView(DetailView): stripe_customer_id = request.user.stripecustomer.id custom_user = request.user + if 'token' in request.session: + ucd = UserCardDetail.get_or_create_user_card_detail( + stripe_customer=self.request.user.stripecustomer, + card_details=card_details_response + ) + UserCardDetail.save_default_card_local( + self.request.user.stripecustomer.stripe_id, + ucd.card_id + ) + else: + card_id = request.session.get('card_id') + user_card_detail = UserCardDetail.objects.get(id=card_id) + card_details_dict = { + 'last4': user_card_detail.last4, + 'brand': user_card_detail.brand, + 'card_id': user_card_detail.card_id + } + if not user_card_detail.preferred: + UserCardDetail.set_default_card( + stripe_api_cus_id=stripe_api_cus_id, + stripe_source_id=user_card_detail.card_id + ) + # Save billing address billing_address_data = request.session.get('billing_address_data') logger.debug('billing_address_data is {}'.format(billing_address_data)) billing_address_data.update({ 'user': custom_user.id }) + user = { 'name': custom_user.name, 'email': custom_user.email, diff --git a/hosting/locale/de/LC_MESSAGES/django.po b/hosting/locale/de/LC_MESSAGES/django.po index d61d09c0..95515355 100644 --- a/hosting/locale/de/LC_MESSAGES/django.po +++ b/hosting/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-05-12 03:53+0530\n" +"POT-Creation-Date: 2018-07-05 23:15+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -274,6 +274,29 @@ msgstr "" msgid "You can always order a new VM by following the link below." msgstr "" +msgid "Card Number" +msgstr "Kreditkartennummer" + +msgid "Expiry Date" +msgstr "Ablaufdatum" + +msgid "CVC" +msgstr "" + +msgid "Card Type" +msgstr "Kartentyp" + +msgid "" +"You are not making any payment yet. After placing your order, you will be " +"taken to the Submit Payment Page." +msgstr "" +"Es wird noch keine Bezahlung vorgenommen. Die Bezahlung wird erst " +"ausgelöst, nachdem Du die Bestellung auf der nächsten Seite bestätigt " +"hast." + +msgid "SUBMIT" +msgstr "ABSENDEN" + msgid "Toggle navigation" msgstr "Umschalten" @@ -439,6 +462,17 @@ msgstr "wird an der Kasse angewendet" msgid "Billing Address" msgstr "Rechnungsadresse" +msgid "" +"Please select one of the cards that you used before or fill in your credit " +"card information below. We are using Stripe for payment and do not store your information in our " +"database." +msgstr "" +"Bitte wähle eine der zuvor genutzten Kreditkarten oder gib Deine " +"Kreditkartendetails unten an. Die Bezahlung wird über " +"Stripe abgewickelt. " +"Wir speichern Deine Kreditkartendetails nicht in unserer Datenbank." + msgid "" "Please fill in your credit card information below. We are using Stripe for payment and do not " @@ -448,28 +482,24 @@ msgstr "" "\"https://stripe.com\" target=\"_blank\">Stripe für die Bezahlung und " "speichern keine Informationen in unserer Datenbank." -msgid "" -"You are not making any payment yet. After submitting your card information, " -"you will be taken to the Confirm Order Page." -msgstr "" -"Es wird noch keine Bezahlung vorgenommen. Die Bezahlung wird erst ausgelöst, " -"nachdem Du die Bestellung auf der nächsten Seite bestätigt hast." +msgid "Last" +msgstr "Letzten" -msgid "SUBMIT" -msgstr "ABSENDEN" - -msgid "Card Number" -msgstr "Kreditkartennummer" - -msgid "Expiry Date" -msgstr "Ablaufdatum" - -msgid "CVC" -msgstr "" - -msgid "Card Type" +msgid "Type" msgstr "Kartentyp" +msgid "SELECT" +msgstr "AUSWÄHLEN" + +msgid "Add a new credit card" +msgstr "Eine neue Kreditkarte hinzufügen" + +msgid "NEW CARD" +msgstr "BEARBEITEN" + +msgid "New Credit Card" +msgstr "Neue Kreditkarte" + msgid "Processing" msgstr "Weiter" @@ -483,13 +513,22 @@ msgid "Password reset" msgstr "Passwort zurücksetzen" msgid "UPDATE" -msgstr "" +msgstr "AKTUALISIEREN" -msgid "Last" -msgstr "" +msgid "REMOVE CARD" +msgstr "KARTE ENTFERNEN" -msgid "Type" -msgstr "Kartentyp" +msgid "Remove Card" +msgstr "Karte entfernen" + +msgid "Do you want to remove this associated card?" +msgstr "Möchtest Du den Schlüssel löschen?" + +msgid "Delete" +msgstr "Löschen" + +msgid "DEFAULT" +msgstr "STANDARD" msgid "No Credit Cards Added" msgstr "Es wurde keine Kreditkarte hinzugefügt" @@ -534,10 +573,7 @@ msgid "Public Key" msgstr "" msgid "Private Key" -msgstr "" - -msgid "Delete" -msgstr "Löschen" +msgstr "Privater Schlüssel" msgid "Delete SSH Key" msgstr "SSH Key löschen" @@ -670,6 +706,36 @@ msgstr "Dein Passwort konnte nicht zurückgesetzt werden." msgid "The reset password link is no longer valid." msgstr "Der Link zum Zurücksetzen Deines Passwortes ist nicht mehr gültig." +msgid "Card deassociation successful" +msgstr "Die Verbindung mit der Karte wurde erfolgreich aufgehoben" + +msgid "You are not permitted to do this operation" +msgstr "Du hast keine Erlaubnis um diese Operation durchzuführen" + +msgid "The selected card does not exist" +msgstr "Die ausgewählte Karte existiert nicht" + +msgid "Billing address updated successfully" +msgstr "Die Rechnungsadresse wurde erfolgreich aktualisiert" + +msgid "You seem to have already added this card" +msgstr "Es scheint, als hättest du diese Karte bereits hinzugefügt" + +#, python-brace-format +msgid "An error occurred while associating the card. Details: {details}" +msgstr "Beim Verbinden der Karte ist ein Fehler aufgetreten. Details: " +"{details}" + +msgid "Successfully associated the card with your account" +msgstr "Die Karte wurde erfolgreich mit deinem Konto verbunden" + +#, python-brace-format +msgid "{user} does not have permission to access the card" +msgstr "{user} hat keine Erlaubnis auf diese Karte zuzugreifen" + +msgid "An error occurred. Details: {}" +msgstr "Ein Fehler ist aufgetreten. Details: {}" + msgid "Invalid credit card" msgstr "Ungültige Kreditkarte" @@ -807,15 +873,6 @@ msgstr "" #~ msgid "Notifications " #~ msgstr "Benachrichtigungen" -#~ msgid "REMOVE CARD" -#~ msgstr "KARTE ENTFERNEN" - -#~ msgid "EDIT CARD" -#~ msgstr "BEARBEITEN" - -#~ msgid "Add a new Card." -#~ msgstr "Neue Kreditkarte hinzufügen." - #~ msgid "You are not making any payment here." #~ msgstr "Es wird noch keine Bezahlung vorgenommen" diff --git a/hosting/management/commands/import_usercarddetails.py b/hosting/management/commands/import_usercarddetails.py new file mode 100644 index 00000000..de5a91f9 --- /dev/null +++ b/hosting/management/commands/import_usercarddetails.py @@ -0,0 +1,45 @@ +from django.core.management.base import BaseCommand + +from hosting.models import UserCardDetail +from membership.models import CustomUser +from utils.stripe_utils import StripeUtils + + +class Command(BaseCommand): + help = '''Imports the usercard details of all customers. Created just for + multiple card support.''' + + def handle(self, *args, **options): + try: + stripe_utils = StripeUtils() + for user in CustomUser.objects.all(): + if hasattr(user, 'stripecustomer'): + if user.stripecustomer: + card_details_resp = stripe_utils.get_card_details( + user.stripecustomer.stripe_id + ) + card_details = card_details_resp['response_object'] + if card_details: + ucd = UserCardDetail.get_or_create_user_card_detail( + stripe_customer=user.stripecustomer, + card_details=card_details + ) + UserCardDetail.save_default_card_local( + user.stripecustomer.stripe_id, + ucd.card_id + ) + print("Saved user card details for {}".format( + user.email + )) + else: + print(" --- Could not get card details for " + "{}".format(user.email)) + print(" --- Error: {}".format( + card_details_resp['error'] + )) + else: + print(" === {} does not have a StripeCustomer object".format( + user.email + )) + except Exception as e: + print(" *** Error occurred. Details {}".format(str(e))) diff --git a/hosting/migrations/0046_usercarddetail.py b/hosting/migrations/0046_usercarddetail.py new file mode 100644 index 00000000..97c1e94a --- /dev/null +++ b/hosting/migrations/0046_usercarddetail.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2018-07-03 20:32 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import utils.mixins + + +class Migration(migrations.Migration): + + dependencies = [ + ('membership', '0007_auto_20180213_0128'), + ('hosting', '0045_auto_20180701_2028'), + ] + + operations = [ + migrations.CreateModel( + name='UserCardDetail', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('last4', models.CharField(max_length=4)), + ('brand', models.CharField(max_length=10)), + ('card_id', models.CharField(blank=True, default='', max_length=100)), + ('fingerprint', models.CharField(max_length=100)), + ('exp_month', models.IntegerField()), + ('exp_year', models.IntegerField()), + ('preferred', models.BooleanField(default=False)), + ('stripe_customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='membership.StripeCustomer')), + ], + options={ + 'permissions': (('view_usercarddetail', 'View User Card'),), + }, + bases=(utils.mixins.AssignPermissionsMixin, models.Model), + ), + ] diff --git a/hosting/models.py b/hosting/models.py index 411bd267..3ae3b0a5 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -1,16 +1,17 @@ -import os import logging -from dateutil.relativedelta import relativedelta +import os +from Crypto.PublicKey import RSA +from dateutil.relativedelta import relativedelta from django.db import models from django.utils import timezone from django.utils.functional import cached_property -from Crypto.PublicKey import RSA from datacenterlight.models import VMPricing, VMTemplate from membership.models import StripeCustomer, CustomUser from utils.models import BillingAddress from utils.mixins import AssignPermissionsMixin +from utils.stripe_utils import StripeUtils logger = logging.getLogger(__name__) @@ -205,3 +206,156 @@ class VMDetail(models.Model): months = relativedelta(end_date, self.created_at).months or 1 end_date = self.created_at + relativedelta(months=months, days=-1) return end_date + + +class UserCardDetail(AssignPermissionsMixin, models.Model): + permissions = ('view_usercarddetail',) + stripe_customer = models.ForeignKey(StripeCustomer) + last4 = models.CharField(max_length=4) + brand = models.CharField(max_length=10) + card_id = models.CharField(max_length=100, blank=True, default='') + fingerprint = models.CharField(max_length=100) + exp_month = models.IntegerField(null=False) + exp_year = models.IntegerField(null=False) + preferred = models.BooleanField(default=False) + + class Meta: + permissions = ( + ('view_usercarddetail', 'View User Card'), + ) + + @classmethod + def create(cls, stripe_customer=None, last4=None, brand=None, + fingerprint=None, exp_month=None, exp_year=None, card_id=None, + preferred=False): + instance = cls.objects.create( + stripe_customer=stripe_customer, last4=last4, brand=brand, + fingerprint=fingerprint, exp_month=exp_month, exp_year=exp_year, + card_id=card_id, preferred=preferred + ) + instance.assign_permissions(stripe_customer.user) + return instance + + @classmethod + def get_all_cards_list(cls, stripe_customer): + """ + Get all the cards of the given customer as a list + + :param stripe_customer: The StripeCustomer object + :return: A list of all cards; an empty list if the customer object is + None + """ + cards_list = [] + if stripe_customer is None: + return cards_list + user_card_details = UserCardDetail.objects.filter( + stripe_customer_id=stripe_customer.id + ).order_by('-preferred', 'id') + for card in user_card_details: + cards_list.append({ + 'last4': card.last4, 'brand': card.brand, 'id': card.id, + 'preferred': card.preferred + }) + return cards_list + + @classmethod + def get_or_create_user_card_detail(cls, stripe_customer, card_details): + """ + A method that checks if a UserCardDetail object exists already + based upon the given card_details and creates it for the given + customer if it does not exist. It returns the UserCardDetail object + matching the given card_details if it exists. + + :param stripe_customer: The given StripeCustomer object to whom the + card object should belong to + :param card_details: A dictionary identifying a given card + :return: UserCardDetail object + """ + try: + if ('fingerprint' in card_details and 'exp_month' in card_details + and 'exp_year' in card_details): + card_detail = UserCardDetail.objects.get( + stripe_customer=stripe_customer, + fingerprint=card_details['fingerprint'], + exp_month=card_details['exp_month'], + exp_year=card_details['exp_year'] + ) + else: + raise UserCardDetail.DoesNotExist() + except UserCardDetail.DoesNotExist: + preferred = False + if 'preferred' in card_details: + preferred = card_details['preferred'] + card_detail = UserCardDetail.create( + stripe_customer=stripe_customer, + last4=card_details['last4'], + brand=card_details['brand'], + fingerprint=card_details['fingerprint'], + exp_month=card_details['exp_month'], + exp_year=card_details['exp_year'], + card_id=card_details['card_id'], + preferred=preferred + ) + return card_detail + + @staticmethod + def set_default_card(stripe_api_cus_id, stripe_source_id): + """ + Sets the given stripe source as the default source for the given + Stripe customer + :param stripe_api_cus_id: Stripe customer id + :param stripe_source_id: The Stripe source id + :return: + """ + stripe_utils = StripeUtils() + cus_response = stripe_utils.get_customer(stripe_api_cus_id) + cu = cus_response['response_object'] + cu.default_source = stripe_source_id + cu.save() + UserCardDetail.save_default_card_local( + stripe_api_cus_id, stripe_source_id + ) + + @staticmethod + def set_default_card_from_stripe(stripe_api_cus_id): + stripe_utils = StripeUtils() + cus_response = stripe_utils.get_customer(stripe_api_cus_id) + cu = cus_response['response_object'] + default_source = cu.default_source + if default_source is not None: + UserCardDetail.save_default_card_local( + stripe_api_cus_id, default_source + ) + + @staticmethod + def save_default_card_local(stripe_api_cus_id, card_id): + stripe_cust = StripeCustomer.objects.get(stripe_id=stripe_api_cus_id) + user_card_detail = UserCardDetail.objects.get( + stripe_customer=stripe_cust, card_id=card_id + ) + for card in stripe_cust.usercarddetail_set.all(): + card.preferred = False + card.save() + user_card_detail.preferred = True + user_card_detail.save() + + @staticmethod + def get_user_card_details(stripe_customer, card_details): + """ + A utility function to check whether a StripeCustomer is already + associated with the card having given details + + :param stripe_customer: + :param card_details: + :return: The UserCardDetails object if it exists, None otherwise + """ + try: + ucd = UserCardDetail.objects.get( + stripe_customer=stripe_customer, + fingerprint=card_details['fingerprint'], + exp_month=card_details['exp_month'], + exp_year=card_details['exp_year'] + ) + return ucd + except UserCardDetail.DoesNotExist: + return None diff --git a/hosting/static/hosting/css/commons.css b/hosting/static/hosting/css/commons.css index 0abfd499..292aeda9 100644 --- a/hosting/static/hosting/css/commons.css +++ b/hosting/static/hosting/css/commons.css @@ -23,6 +23,13 @@ margin: 0 auto; max-width: 1120px; } +.container-table{ + margin-top: 35px; + overflow-y: hidden; +} +.container-table table{ + overflow-y: auto; +} .borderless td { border: none !important; } @@ -35,6 +42,19 @@ color: transparent; } +.inline-headers h3, .inline-headers h4 { + display: inline-block; + vertical-align: baseline; +} + +.space-above { + margin-top: 4%; +} + +.space-above-big { + margin-top: 20%; +} + .table>tbody>tr>td{ vertical-align: middle; } @@ -274,6 +294,26 @@ font-size: 16px; } +.payment-container .credit-card-info { + padding-bottom: 15px; + border-bottom: 1px solid #eee; +} + +.credit-card-info { + display: flex; +} + +.credit-card-info .align-bottom{ + align-self: flex-end; + padding-right: 0 !important; +} + +.another-card-text { + padding: 20px 0; + font-size: 18px; + font-weight: 700; +} + .credit-card-form { max-width: 360px; } @@ -293,8 +333,15 @@ text-decoration: none; } -.settings-container .credit-card-details-opt { - padding-top: 15px; +.settings-container .new-card-head { + margin-top: 40px; + margin-bottom: 30px; +} + +.settings-container .new-card-head h4 { + font-size: 15px; + margin-top: 8px; + font-weight: 600; } .caps-link .svg-img { @@ -313,7 +360,11 @@ .settings-container .btn-vm-contact { font-weight: 600; font-size: 13px; - /* padding: 4px 15px; */ +} + +.settings-container .choice-btn { + letter-spacing: 2px; + min-width: 127px; } .btn-wide { @@ -355,6 +406,15 @@ fill: #999; } +.card-details-box { + border: 1px solid #eee; + padding: 5px 25px 25px; +} + +.thick-hr { + border-top: 5px solid #eee; +} + .locale_date { opacity: 0; } @@ -370,4 +430,13 @@ .thin-hr { margin-top: 10px; margin-bottom: 10px; +} + + +.new-card-head { + margin-top: 10px; +} +.new-card-button-margin button{ + margin-top: 5px; + margin-bottom: 5px; } \ No newline at end of file diff --git a/hosting/static/hosting/css/virtual-machine.css b/hosting/static/hosting/css/virtual-machine.css index 2ae4577a..1c50776d 100644 --- a/hosting/static/hosting/css/virtual-machine.css +++ b/hosting/static/hosting/css/virtual-machine.css @@ -269,7 +269,7 @@ border: 2px solid #A3C0E2; padding: 5px 25px; font-size: 12px; - letter-spacing: 1.3px; + letter-spacing: 2px; } .btn-vm-contact:hover, .btn-vm-contact:focus { background: #fff; diff --git a/hosting/static/hosting/js/payment.js b/hosting/static/hosting/js/payment.js index ef59416b..4934fdd3 100644 --- a/hosting/static/hosting/js/payment.js +++ b/hosting/static/hosting/js/payment.js @@ -195,5 +195,11 @@ $(document).ready(function () { $(element).closest('.form-group').append(error); } }); + + $('.credit-card-info .btn.choice-btn').click(function(){ + var id = this.dataset['id_card']; + $('#id_card').val(id); + $('#billing-form').submit(); + }); }); diff --git a/hosting/templates/hosting/includes/_card_input.html b/hosting/templates/hosting/includes/_card_input.html new file mode 100644 index 00000000..8cf2d55b --- /dev/null +++ b/hosting/templates/hosting/includes/_card_input.html @@ -0,0 +1,50 @@ +{% load i18n %} + +
+ + +
+
+
+ +
+
+
+
+ +
+
+
+ +
+
+
+
+ + +
+
+
+
+ {% if not messages and not form.non_field_errors %} +

+ {% trans "You are not making any payment yet. After placing your order, you will be taken to the Submit Payment Page." %} +

+ {% endif %} +
+ {% for message in messages %} + {% if 'failed_payment' in message.tags or 'make_charge_error' in message.tags or 'error' in message.tags %} +
    +
  • {{ message|safe }}

  • +
+ {% endif %} + {% endfor %} +
+
+ +
+ +
+

+
+
\ No newline at end of file diff --git a/hosting/templates/hosting/payment.html b/hosting/templates/hosting/payment.html index afcf6373..e09775cf 100644 --- a/hosting/templates/hosting/payment.html +++ b/hosting/templates/hosting/payment.html @@ -105,114 +105,65 @@

{%trans "Billing Address"%}


- {% for field in form %} {% csrf_token %} + {% for field in form %} {% bootstrap_field field show_label=False type='fields'%} {% endfor %}
-
-
-

{%trans "Credit Card"%}

-
+
+ {% with card_list_len=cards_list|length %} +

{%trans "Credit Card"%}

+
+
+

+ {% if card_list_len > 0 %} + {% blocktrans %}Please select one of the cards that you used before or fill in your credit card information below. We are using Stripe for payment and do not store your information in our database.{% endblocktrans %} + {% else %} + {% blocktrans %}Please fill in your credit card information below. We are using Stripe for payment and do not store your information in our database.{% endblocktrans %} + {% endif %} +

-

- {% blocktrans %}Please fill in your credit card information below. We are using Stripe for payment and do not store your information in our database.{% endblocktrans %} -

-
- {% if credit_card_data.last4 %} -
-
Credit Card
-
Last 4: *****{{credit_card_data.last4}}
-
Type: {{credit_card_data.cc_brand}}
- -
- {% if not messages and not form.non_field_errors %} -

- {% trans "You are not making any payment yet. After submitting your card information, you will be taken to the Confirm Order Page." %} -

- {% endif %} -
- {% for message in messages %} - {% if 'failed_payment' or 'make_charge_error' in message.tags %} -
    -
  • -

    {{ message|safe }}

    -
  • -
- {% endif %} - {% endfor %} - {% for error in form.non_field_errors %} -

- {{ error|escape }} -

- {% endfor %} + {% for card in cards_list %} +
+
+
{% trans "Credit Card" %}
+
{% trans "Last" %} 4: ***** {{card.last4}}
+
{% trans "Type" %}: {{card.brand}}
-
- + - {% else %} -
- -
-
-
- -
-
-
-
- -
-
-
- -
-
-
-
- - -
-
+
+ {% endfor %} + {% if card_list_len > 0 %} +
+
+
+

{% trans "Add a new credit card" %}

-
- {% if not messages and not form.non_field_errors %} -

- {% trans "You are not making any payment yet. After submitting your card information, you will be taken to the Confirm Order Page." %} -

- {% endif %} -
- {% for message in messages %} - {% if 'failed_payment' or 'make_charge_error' in message.tags %} -
    -
  • -

    {{ message|safe }}

    -
  • -
- {% endif %} - {% endfor %} - - {% for error in form.non_field_errors %} -

- {{ error|escape }} -

- {% endfor %} -
-
- -
+
+
- -
-

-
- - {% endif %} -
+
+
+
+
+
+

{%trans "New Credit Card" %}

+
+ {% include "hosting/includes/_card_input.html" %} +
+
+ {% else%} + {% include "hosting/includes/_card_input.html" %} + {% endif %}
+ {% endwith %}
@@ -232,7 +183,7 @@ })(); {%endif%} - +{% comment "Looks as if no more used. To test..." %} {% if credit_card_data.last4 and credit_card_data.cc_brand %} {%endif%} - +{% endcomment %} {%endblock%} diff --git a/hosting/templates/hosting/settings.html b/hosting/templates/hosting/settings.html index 0bafe8e5..62dcc947 100644 --- a/hosting/templates/hosting/settings.html +++ b/hosting/templates/hosting/settings.html @@ -7,6 +7,7 @@ {% block content %}
+ {% include 'hosting/includes/_messages.html' %}

{% trans "My Settings" %}

@@ -14,7 +15,7 @@
-

{%trans "Billing Address"%}

+

{%trans "Billing Address" %}


{% for field in form %} @@ -22,108 +23,97 @@ {% bootstrap_field field show_label=False type='fields' bound_css_class='' %} {% endfor %}
- +
-

{%trans "Credit Card"%}

+

{%trans "Credit Card" %}


- {% if credit_card_data.last4 %} + {% with card_list_len=cards_list|length %} + {% for card in cards_list %}
{% trans "Credit Card" %}
-
{% trans "Last" %} 4: *****{{credit_card_data.last4}}
-
{% trans "Type" %}: {{credit_card_data.cc_brand}}
- {% comment %} +
{% trans "Last" %} 4: ***** {{card.last4}}
+
{% trans "Type" %}: {{card.brand}}
+ {% if card_list_len > 1 %}
- {% trans "REMOVE CARD" %} + {% trans "REMOVE CARD" %} +
+ {% endif %}
- {% trans "EDIT CARD" %} + {% if card.preferred %} + {% trans "DEFAULT" %} + {% else %} +
+ {% csrf_token %} + + {% trans "SELECT" %} +
+ {% endif %}
- {% endcomment %}
- {% else %} + {% empty %}

{% trans "No Credit Cards Added" %}

{% blocktrans %}We are using Stripe for payment and do not store your information in our database.{% endblocktrans %}

+ {% endfor %} + {% endwith %} - {% comment %} -

{% trans "Add a new Card." %}

-

- {% blocktrans %}Please fill in your credit card information below. We are using Stripe for payment and do not store your information in our database.{% endblocktrans %} -

-
- -
-
- -
-
-
-
- -
-
-
- -
-
-
-
- - -
-
- -
- {% if not messages and not form.non_field_errors %} -

- {% blocktrans %}You are not making any payment here.{% endblocktrans %} -

- {% endif %} -
- {% for message in messages %} - {% if 'failed_payment' or 'make_charge_error' in message.tags %} -
  • -

    {{ message|safe }}

    -
- {% endif %} - {% endfor %} - - {% for error in form.non_field_errors %} -

- {{ error|escape }} -

- {% endfor %} -
-
-
- -
-
-
- -
-

-
-
- {% endcomment %} - {% endif %} +
+
+
+

{% trans "Add a new credit card" %}

+
+
+ +
+
+
+
+
+
+

{%trans "New Credit Card" %}

+
+ {% include "hosting/includes/_card_input.html" %} +
+
- {% comment %} {% if stripe_key %} {% get_current_language as LANGUAGE_CODE %} @@ -137,13 +127,4 @@ })(); {%endif%} - - {% if credit_card_data.last4 and credit_card_data.cc_brand %} - - {%endif%} - {% endcomment %} {%endblock%} diff --git a/hosting/urls.py b/hosting/urls.py index 2112c493..32ef8400 100644 --- a/hosting/urls.py +++ b/hosting/urls.py @@ -43,6 +43,8 @@ urlpatterns = [ name='choice_ssh_keys'), url(r'delete_ssh_key/(?P\d+)/?$', SSHKeyDeleteView.as_view(), name='delete_ssh_key'), + url(r'delete_card/(?P\d+)/?$', SettingsView.as_view(), + name='delete_card'), url(r'create_ssh_key/?$', SSHKeyCreateView.as_view(), name='create_ssh_key'), url(r'^notifications/$', NotificationsView.as_view(), diff --git a/hosting/views.py b/hosting/views.py index e5383535..6af1885b 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -15,11 +15,12 @@ from django.http import ( Http404, HttpResponseRedirect, HttpResponse, JsonResponse ) from django.shortcuts import redirect, render +from django.utils.decorators import method_decorator +from django.utils.html import escape from django.utils.http import urlsafe_base64_decode from django.utils.safestring import mark_safe from django.utils.translation import get_language, ugettext_lazy as _ from django.utils.translation import ugettext -from django.utils.decorators import method_decorator from django.views.decorators.cache import never_cache from django.views.generic import ( View, CreateView, FormView, ListView, DetailView, DeleteView, @@ -33,6 +34,7 @@ from stored_messages.settings import stored_messages_settings from datacenterlight.models import VMTemplate, VMPricing from datacenterlight.utils import create_vm, get_cms_integration +from hosting.models import UserCardDetail from membership.models import CustomUser, StripeCustomer from opennebula_api.models import OpenNebulaManager from opennebula_api.serializers import ( @@ -43,7 +45,7 @@ from utils.forms import ( BillingAddressForm, PasswordResetRequestForm, UserBillingAddressForm, ResendActivationEmailForm ) -from utils.hosting_utils import get_vm_price_with_vat +from utils.hosting_utils import get_vm_price_with_vat, HostingUtils from utils.mailer import BaseEmail from utils.stripe_utils import StripeUtils from utils.tasks import send_plain_email_task @@ -565,6 +567,7 @@ class SettingsView(LoginRequiredMixin, FormView): template_name = "hosting/settings.html" login_url = reverse_lazy('hosting:login') form_class = BillingAddressForm + permission_required = ['view_usercarddetail'] def get_form(self, form_class): """ @@ -579,33 +582,124 @@ class SettingsView(LoginRequiredMixin, FormView): context = super(SettingsView, self).get_context_data(**kwargs) # Get user user = self.request.user - # Get user last order - last_hosting_order = HostingOrder.objects.filter( - customer__user=user).last() - # If user has already an hosting order, get the credit card data from - # it - if last_hosting_order: - credit_card_data = last_hosting_order.get_cc_data() - context.update({ - 'credit_card_data': credit_card_data if credit_card_data else None, - }) + stripe_customer = None + if hasattr(user, 'stripecustomer'): + stripe_customer = user.stripecustomer + cards_list = UserCardDetail.get_all_cards_list( + stripe_customer=stripe_customer + ) context.update({ + 'cards_list': cards_list, 'stripe_key': settings.STRIPE_API_PUBLIC_KEY }) return context def post(self, request, *args, **kwargs): + if 'card' in request.POST and request.POST['card'] is not '': + card_id = escape(request.POST['card']) + user_card_detail = UserCardDetail.objects.get(id=card_id) + UserCardDetail.set_default_card( + stripe_api_cus_id=request.user.stripecustomer.stripe_id, + stripe_source_id=user_card_detail.card_id + ) + msg = _( + ("Your {brand} card ending in {last4} set as " + "default card").format( + brand=user_card_detail.brand, + last4=user_card_detail.last4 + ) + ) + messages.add_message(request, messages.SUCCESS, msg) + return HttpResponseRedirect(reverse_lazy('hosting:settings')) + if 'delete_card' in request.POST: + try: + card = UserCardDetail.objects.get(pk=self.kwargs.get('pk')) + if (request.user.has_perm(self.permission_required[0], card) + and + request.user + .stripecustomer + .usercarddetail_set + .count() > 1): + if card.card_id is not None: + stripe_utils = StripeUtils() + stripe_utils.dissociate_customer_card( + request.user.stripecustomer.stripe_id, + card.card_id + ) + if card.preferred: + UserCardDetail.set_default_card_from_stripe( + request.user.stripecustomer.stripe_id + ) + card.delete() + msg = _("Card deassociation successful") + messages.add_message(request, messages.SUCCESS, msg) + else: + msg = _("You are not permitted to do this operation") + messages.add_message(request, messages.ERROR, msg) + except UserCardDetail.DoesNotExist: + msg = _("The selected card does not exist") + messages.add_message(request, messages.ERROR, msg) + return HttpResponseRedirect(reverse_lazy('hosting:settings')) form = self.get_form() if form.is_valid(): - billing_address_data = form.cleaned_data - billing_address_data.update({ - 'user': self.request.user.id - }) - billing_address_user_form = UserBillingAddressForm( - instance=self.request.user.billing_addresses.first(), - data=billing_address_data) - billing_address_user_form.save() + if 'billing-form' in request.POST: + billing_address_data = form.cleaned_data + billing_address_data.update({ + 'user': self.request.user.id + }) + billing_address_user_form = UserBillingAddressForm( + instance=self.request.user.billing_addresses.first(), + data=billing_address_data) + billing_address_user_form.save() + msg = _("Billing address updated successfully") + messages.add_message(request, messages.SUCCESS, msg) + else: + token = form.cleaned_data.get('token') + stripe_utils = StripeUtils() + card_details = stripe_utils.get_cards_details_from_token( + token + ) + if not card_details.get('response_object'): + form.add_error("__all__", card_details.get('error')) + return self.render_to_response(self.get_context_data()) + stripe_customer = StripeCustomer.get_or_create( + email=request.user.email, token=token + ) + card = card_details['response_object'] + if UserCardDetail.get_user_card_details(stripe_customer, card): + msg = _('You seem to have already added this card') + messages.add_message(request, messages.ERROR, msg) + else: + acc_result = stripe_utils.associate_customer_card( + request.user.stripecustomer.stripe_id, token + ) + if acc_result['response_object'] is None: + msg = _( + 'An error occurred while associating the card.' + ' Details: {details}'.format( + details=acc_result['error'] + ) + ) + messages.add_message(request, messages.ERROR, msg) + return self.render_to_response(self.get_context_data()) + preferred = False + if stripe_customer.usercarddetail_set.count() == 0: + preferred = True + UserCardDetail.create( + stripe_customer=stripe_customer, + last4=card['last4'], + brand=card['brand'], + fingerprint=card['fingerprint'], + exp_month=card['exp_month'], + exp_year=card['exp_year'], + card_id=card['card_id'], + preferred=preferred + ) + msg = _( + "Successfully associated the card with your account" + ) + messages.add_message(request, messages.SUCCESS, msg) return self.render_to_response(self.get_context_data()) else: billing_address_data = form.cleaned_data @@ -638,24 +732,19 @@ class PaymentVMView(LoginRequiredMixin, FormView): context = super(PaymentVMView, self).get_context_data(**kwargs) # Get user user = self.request.user - - # Get user last order - last_hosting_order = HostingOrder.objects.filter( - customer__user=user).last() - - # If user has already an hosting order, get the credit card data from - # it - if last_hosting_order: - credit_card_data = last_hosting_order.get_cc_data() - context.update({ - 'credit_card_data': credit_card_data if credit_card_data else None, - }) - + if hasattr(user, 'stripecustomer'): + stripe_customer = user.stripecustomer + else: + stripe_customer = None + cards_list = UserCardDetail.get_all_cards_list( + stripe_customer=stripe_customer + ) context.update({ 'stripe_key': settings.STRIPE_API_PUBLIC_KEY, 'vm_pricing': VMPricing.get_vm_pricing_by_name( self.request.session.get('specs', {}).get('pricing_name') ), + 'cards_list': cards_list, }) return context @@ -664,6 +753,10 @@ class PaymentVMView(LoginRequiredMixin, FormView): def get(self, request, *args, **kwargs): if 'next' in request.session: del request.session['next'] + HostingUtils.clear_items_from_list( + request.session, + ['token', 'card_id', 'customer', 'user'] + ) return self.render_to_response(self.get_context_data()) @method_decorator(decorators) @@ -674,23 +767,51 @@ class PaymentVMView(LoginRequiredMixin, FormView): billing_address_data = form.cleaned_data token = form.cleaned_data.get('token') owner = self.request.user - # Get or create stripe customer - customer = StripeCustomer.get_or_create(email=owner.email, - token=token) - if not customer: - msg = _("Invalid credit card") - messages.add_message( - self.request, messages.ERROR, msg, - extra_tags='make_charge_error') - return HttpResponseRedirect( - reverse('hosting:payment') + '#payment_error') - + if token is '': + card_id = form.cleaned_data.get('card') + customer = owner.stripecustomer + try: + user_card_detail = UserCardDetail.objects.get(id=card_id) + if not request.user.has_perm( + 'view_usercarddetail', user_card_detail + ): + raise UserCardDetail.DoesNotExist( + _("{user} does not have permission to access the " + "card").format(user=request.user.email) + ) + except UserCardDetail.DoesNotExist as e: + ex = str(e) + logger.error("Card Id: {card_id}, Exception: {ex}".format( + card_id=card_id, ex=ex + ) + ) + msg = _("An error occurred. Details: {}".format(ex)) + messages.add_message( + self.request, messages.ERROR, msg, + extra_tags='make_charge_error' + ) + return HttpResponseRedirect( + reverse('hosting:payment') + '#payment_error' + ) + request.session['card_id'] = user_card_detail.id + else: + # Get or create stripe customer + customer = StripeCustomer.get_or_create( + email=owner.email, token=token + ) + if not customer: + msg = _("Invalid credit card") + messages.add_message( + self.request, messages.ERROR, msg, + extra_tags='make_charge_error') + return HttpResponseRedirect( + reverse('hosting:payment') + '#payment_error') + request.session['token'] = token request.session['billing_address_data'] = billing_address_data - request.session['token'] = token - request.session['customer'] = customer.stripe_id return HttpResponseRedirect("{url}?{query_params}".format( url=reverse('hosting:order-confirmation'), - query_params='page=payment')) + query_params='page=payment') + ) else: return self.form_invalid(form) @@ -723,12 +844,6 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView): ).get_context_data(**kwargs) obj = self.get_object() owner = self.request.user - stripe_api_cus_id = self.request.session.get('customer') - stripe_utils = StripeUtils() - card_details = stripe_utils.get_card_details( - stripe_api_cus_id, - self.request.session.get('token') - ) if self.request.GET.get('page') == 'payment': context['page_header_text'] = _('Confirm Order') @@ -784,8 +899,9 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView): context['vm']['price'] = price context['vm']['discount'] = discount context['vm']['vat_percent'] = vat_percent - context['vm']['total_price'] = price + \ - vat - discount['amount'] + context['vm']['total_price'] = ( + price + vat - discount['amount'] + ) except WrongIdError: messages.error( self.request, @@ -800,17 +916,25 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView): _('In order to create a VM, you need to create/upload ' 'your SSH KEY first.') ) - elif not card_details.get('response_object'): - # new order, failed to get card details - context['failed_payment'] = True - context['card_details'] = card_details else: # new order, confirm payment + if 'token' in self.request.session: + token = self.request.session['token'] + stripe_utils = StripeUtils() + card_details = stripe_utils.get_cards_details_from_token( + token + ) + if not card_details.get('response_object'): + return HttpResponseRedirect(reverse('hosting:payment')) + card_details_response = card_details['response_object'] + context['cc_last4'] = card_details_response['last4'] + context['cc_brand'] = card_details_response['brand'] + else: + card_id = self.request.session.get('card_id') + card_detail = UserCardDetail.objects.get(id=card_id) + context['cc_last4'] = card_detail.last4 + context['cc_brand'] = card_detail.brand context['site_url'] = reverse('hosting:create_virtual_machine') - context['cc_last4'] = card_details.get('response_object').get( - 'last4') - context['cc_brand'] = card_details.get('response_object').get( - 'cc_brand') context['vm'] = self.request.session.get('specs') return context @@ -821,7 +945,9 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView): return HttpResponseRedirect( reverse('hosting:create_virtual_machine') ) - if 'token' not in self.request.session: + + if ('token' not in self.request.session and + 'card_id' not in self.request.session): return HttpResponseRedirect(reverse('hosting:payment')) self.object = self.get_object() context = self.get_context_data(object=self.object) @@ -840,24 +966,68 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView): def post(self, request): template = request.session.get('template') specs = request.session.get('specs') + stripe_utils = StripeUtils() # We assume that if the user is here, his/her StripeCustomer # object already exists stripe_customer_id = request.user.stripecustomer.id billing_address_data = request.session.get('billing_address_data') vm_template_id = template.get('id', 1) - stripe_api_cus_id = self.request.session.get('customer') - # Make stripe charge to a customer - stripe_utils = StripeUtils() - card_details = stripe_utils.get_card_details(stripe_api_cus_id, - request.session.get( - 'token')) - if not card_details.get('response_object'): - msg = card_details.get('error') - messages.add_message(self.request, messages.ERROR, msg, - extra_tags='failed_payment') - return HttpResponseRedirect( - reverse('datacenterlight:payment') + '#payment_error') - card_details_dict = card_details.get('response_object') + stripe_api_cus_id = request.user.stripecustomer.stripe_id + if 'token' in self.request.session: + card_details = stripe_utils.get_cards_details_from_token( + request.session['token'] + ) + if not card_details.get('response_object'): + return HttpResponseRedirect(reverse('hosting:payment')) + card_details_response = card_details['response_object'] + card_details_dict = { + 'last4': card_details_response['last4'], + 'brand': card_details_response['brand'], + 'card_id': card_details_response['card_id'] + } + ucd = UserCardDetail.get_user_card_details( + request.user.stripecustomer, card_details_response + ) + if not ucd: + acc_result = stripe_utils.associate_customer_card( + stripe_api_cus_id, request.session['token'], + set_as_default=True + ) + if acc_result['response_object'] is None: + msg = _( + 'An error occurred while associating the card.' + ' Details: {details}'.format( + details=acc_result['error'] + ) + ) + messages.add_message(self.request, messages.ERROR, msg, + extra_tags='failed_payment') + response = { + 'status': False, + 'redirect': "{url}#{section}".format( + url=reverse('hosting:payment'), + section='payment_error'), + 'msg_title': str(_('Error.')), + 'msg_body': str( + _('There was a payment related error.' + ' On close of this popup, you will be redirected' + ' back to the payment page.') + ) + } + return JsonResponse(response) + else: + card_id = request.session.get('card_id') + user_card_detail = UserCardDetail.objects.get(id=card_id) + card_details_dict = { + 'last4': user_card_detail.last4, + 'brand': user_card_detail.brand, + 'card_id': user_card_detail.card_id + } + if not user_card_detail.preferred: + UserCardDetail.set_default_card( + stripe_api_cus_id=stripe_api_cus_id, + stripe_source_id=user_card_detail.card_id + ) cpu = specs.get('cpu') memory = specs.get('memory') disk_size = specs.get('disk_size') @@ -882,6 +1052,12 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView): # Check if the subscription was approved and is active if (stripe_subscription_obj is None or stripe_subscription_obj.status != 'active'): + # At this point, we have created a Stripe API card and + # associated it with the customer; but the transaction failed + # due to some reason. So, we would want to dissociate this card + # here. + # ... + msg = subscription_result.get('error') messages.add_message(self.request, messages.ERROR, msg, extra_tags='failed_payment') @@ -894,10 +1070,20 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView): 'msg_body': str( _('There was a payment related error.' ' On close of this popup, you will be redirected back to' - ' the payment page.')) + ' the payment page.') + ) } return JsonResponse(response) + if 'token' in request.session: + ucd = UserCardDetail.get_or_create_user_card_detail( + stripe_customer=self.request.user.stripecustomer, + card_details=card_details_response + ) + UserCardDetail.save_default_card_local( + self.request.user.stripecustomer.stripe_id, + ucd.card_id + ) user = { 'name': self.request.user.name, 'email': self.request.user.email, diff --git a/membership/models.py b/membership/models.py index b3cbcd91..c5e83735 100644 --- a/membership/models.py +++ b/membership/models.py @@ -222,21 +222,28 @@ class StripeCustomer(models.Model): # check if user is not in stripe but in database customer = stripe_utils.check_customer(stripe_customer.stripe_id, stripe_customer.user, token) - - if not customer.sources.data: - stripe_utils.update_customer_token(customer, token) + if "deleted" in customer and customer["deleted"]: + raise StripeCustomer.DoesNotExist() return stripe_customer - except StripeCustomer.DoesNotExist: user = CustomUser.objects.get(email=email) stripe_utils = StripeUtils() stripe_data = stripe_utils.create_customer(token, email, user.name) if stripe_data.get('response_object'): stripe_cus_id = stripe_data.get('response_object').get('id') - - stripe_customer = StripeCustomer.objects. \ - create(user=user, stripe_id=stripe_cus_id) - + if hasattr(user, 'stripecustomer'): + # User already had a Stripe account and we are here + # because the account was deleted in dashboard + # So, we simply update the stripe_id + user.stripecustomer.stripe_id = stripe_cus_id + user.stripecustomer.save() + stripe_customer = user.stripecustomer + else: + # The user never had an associated Stripe account + # So, create one + stripe_customer = StripeCustomer.objects.create( + user=user, stripe_id=stripe_cus_id + ) return stripe_customer else: return None diff --git a/utils/forms.py b/utils/forms.py index f8a6d103..fe5dc282 100644 --- a/utils/forms.py +++ b/utils/forms.py @@ -117,6 +117,7 @@ class EditCreditCardForm(forms.Form): class BillingAddressForm(forms.ModelForm): token = forms.CharField(widget=forms.HiddenInput(), required=False) + card = forms.CharField(widget=forms.HiddenInput(), required=False) class Meta: model = BillingAddress @@ -136,6 +137,31 @@ class BillingAddressFormSignup(BillingAddressForm): email = forms.EmailField(label=_('Email Address')) field_order = ['name', 'email'] + class Meta: + model = BillingAddress + fields = ['name', 'email', 'cardholder_name', 'street_address', + 'city', 'postal_code', 'country'] + labels = { + 'name': 'Name', + 'email': _('Email'), + 'cardholder_name': _('Cardholder Name'), + 'street_address': _('Street Address'), + 'city': _('City'), + 'postal_code': _('Postal Code'), + 'Country': _('Country'), + } + + def clean_email(self): + email = self.cleaned_data.get('email') + try: + CustomUser.objects.get(email=email) + raise forms.ValidationError( + _("The email {} is already registered with us. Please reset " + "your password and access your account.".format(email)) + ) + except CustomUser.DoesNotExist: + return email + class UserBillingAddressForm(forms.ModelForm): user = forms.ModelChoiceField(queryset=CustomUser.objects.all(), diff --git a/utils/hosting_utils.py b/utils/hosting_utils.py index 36964867..c0494b90 100644 --- a/utils/hosting_utils.py +++ b/utils/hosting_utils.py @@ -128,3 +128,23 @@ def get_vm_price_with_vat(cpu, memory, ssd_size, hdd_size=0, 'amount': float(pricing.discount_amount), } return float(price), float(vat), float(vat_percent), discount + + +class HostingUtils: + @staticmethod + def clear_items_from_list(from_list, items_list): + """ + A utility function to clear items from a given list. + Useful when deleting items in bulk from session. + e.g.: + HostingUtils.clear_items_from_list( + request.session, + ['token', 'billing_address_data', 'card_id',] + ) + :param from_list: + :param items_list: + :return: + """ + for var in items_list: + if var in from_list: + del from_list[var] diff --git a/utils/locale/de/LC_MESSAGES/django.po b/utils/locale/de/LC_MESSAGES/django.po index f18fc9c2..ae679d16 100644 --- a/utils/locale/de/LC_MESSAGES/django.po +++ b/utils/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-10-10 21:35+0530\n" +"POT-Creation-Date: 2018-07-05 23:18+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -777,11 +777,18 @@ msgstr "" msgid "Email Address" msgstr "" -msgid "Street Building" -msgstr "" - msgid "Email" +msgstr "E-Mail" + +msgid "" +"The email {} is already registered with us. Please reset your password and " +"access your account." msgstr "" +"Diese E-Mail-Adresse existiert bereits. Bitte setze dein Passwort zurück um " +"auf dein Konto zuzugreifen." + +msgid "Street Building" +msgstr "Gebäude" msgid "Phone number" msgstr "Telefon" diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py index 3809e138..2045df8e 100644 --- a/utils/stripe_utils.py +++ b/utils/stripe_utils.py @@ -78,6 +78,22 @@ class StripeUtils(object): customer.source = token customer.save() + @handleStripeError + def associate_customer_card(self, stripe_customer_id, token, + set_as_default=False): + customer = stripe.Customer.retrieve(stripe_customer_id) + card = customer.sources.create(source=token) + if set_as_default: + customer.default_source = card.id + customer.save() + return True + + @handleStripeError + def dissociate_customer_card(self, stripe_customer_id, card_id): + customer = stripe.Customer.retrieve(stripe_customer_id) + card = customer.sources.retrieve(card_id) + card.delete() + @handleStripeError def update_customer_card(self, customer_id, token): customer = stripe.Customer.retrieve(customer_id) @@ -93,32 +109,47 @@ class StripeUtils(object): return new_card_data @handleStripeError - def get_card_details(self, customer_id, token): + def get_card_details(self, customer_id): customer = stripe.Customer.retrieve(customer_id) credit_card_raw_data = customer.sources.data.pop() card_details = { 'last4': credit_card_raw_data.last4, - 'brand': credit_card_raw_data.brand + 'brand': credit_card_raw_data.brand, + 'exp_month': credit_card_raw_data.exp_month, + 'exp_year': credit_card_raw_data.exp_year, + 'fingerprint': credit_card_raw_data.fingerprint, + 'card_id': credit_card_raw_data.id } return card_details - def check_customer(self, id, user, token): - customers = self.stripe.Customer.all() - if not customers.get('data'): + @handleStripeError + def get_cards_details_from_token(self, token): + stripe_token = stripe.Token.retrieve(token) + card_details = { + 'last4': stripe_token.card.last4, + 'brand': stripe_token.card.brand, + 'exp_month': stripe_token.card.exp_month, + 'exp_year': stripe_token.card.exp_year, + 'fingerprint': stripe_token.card.fingerprint, + 'card_id': stripe_token.card.id + } + return card_details + + def check_customer(self, stripe_cus_api_id, user, token): + try: + customer = stripe.Customer.retrieve(stripe_cus_api_id) + except stripe.InvalidRequestError: customer = self.create_customer(token, user.email, user.name) - else: - try: - customer = stripe.Customer.retrieve(id) - except stripe.InvalidRequestError: - customer = self.create_customer(token, user.email, user.name) - user.stripecustomer.stripe_id = customer.get( - 'response_object').get('id') - user.stripecustomer.save() + user.stripecustomer.stripe_id = customer.get( + 'response_object').get('id') + user.stripecustomer.save() + if type(customer) is dict: + customer = customer['response_object'] return customer @handleStripeError - def get_customer(self, id): - customer = stripe.Customer.retrieve(id) + def get_customer(self, stripe_api_cus_id): + customer = stripe.Customer.retrieve(stripe_api_cus_id) # data = customer.get('response_object') return customer @@ -296,3 +327,15 @@ class StripeUtils(object): cpu=cpu, memory=memory, disk_size=disk_size) + + @handleStripeError + def set_subscription_meta_data(self, subscription_id, meta_data): + """ + Adds VM metadata to a subscription + :param subscription_id: Stripe identifier for the subscription + :param meta_data: A dict of meta data to be added + :return: + """ + subscription = stripe.Subscription.retrieve(subscription_id) + subscription.metadata = meta_data + subscription.save()