Merge pull request #530 from ungleich/task/3747/multiple_cards_support

Task/3747/multiple cards support
This commit is contained in:
Pcoder 2018-07-07 02:28:29 +02:00 committed by GitHub
commit 0caf3da3bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1251 additions and 485 deletions

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2018-03-30 23:22+0000\n"
"Last-Translator: b'Anonymous User <coder.purple+25@gmail.com>'\n" "Last-Translator: b'Anonymous User <coder.purple+25@gmail.com>'\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -329,6 +329,17 @@ msgstr "wird an der Kasse angewendet"
msgid "Credit Card" msgid "Credit Card"
msgstr "Kreditkarte" msgstr "Kreditkarte"
msgid ""
"Please select one of the cards that you used before or fill in your credit "
"card information below. We are using <a href=\"https://stripe.com\" target="
"\"_blank\">Stripe</a> 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 "
"<a href=\"https://stripe.com\" target=\"_blank\">Stripe</a> abgewickelt. "
"Wir speichern Deine Kreditkartendetails nicht in unserer Datenbank."
msgid "" msgid ""
"Please fill in your credit card information below. We are using <a href=" "Please fill in your credit card information below. We are using <a href="
"\"https://stripe.com\" target=\"_blank\">Stripe</a> for payment and do not " "\"https://stripe.com\" target=\"_blank\">Stripe</a> for payment and do not "
@ -338,31 +349,23 @@ msgstr ""
"\"https://stripe.com\" target=\"_blank\">Stripe</a> für die Bezahlung und " "\"https://stripe.com\" target=\"_blank\">Stripe</a> für die Bezahlung und "
"speichern keine Informationen in unserer Datenbank." "speichern keine Informationen in unserer Datenbank."
msgid "" msgid "Last"
"You are not making any payment yet. After submitting your card information, " msgstr "Letzten"
"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" msgid "Type"
msgstr "Kreditkartennummer" msgstr "Typ"
msgid "Expiry Date" msgid "SELECT"
msgstr "Ablaufdatum" msgstr "AUSWÄHLEN"
msgid "CVC" msgid "Add a new credit card"
msgstr "" msgstr "Eine neue Kreditkarte hinzufügen"
msgid "Card Type" msgid "NEW CARD"
msgstr "Kartentyp" msgstr "NEUE KARTE"
msgid "" msgid "New Credit Card"
"You are not making any payment yet. After placing your order, you will be " msgstr "Neue Kreditkarte"
"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 "Processing" msgid "Processing"
msgstr "Weiter" msgstr "Weiter"
@ -516,6 +519,13 @@ msgstr "Ungültige Speicher-Grösse"
msgid "Incorrect pricing name. Please contact support{support_email}" msgid "Incorrect pricing name. Please contact support{support_email}"
msgstr "" 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" msgid "Confirm Order"
msgstr "Bestellung Bestätigen" msgstr "Bestellung Bestätigen"
@ -529,6 +539,11 @@ msgstr ""
"Es ist ein Fehler bei der Zahlung betreten. Du wirst nach dem Schliessen vom " "Es ist ein Fehler bei der Zahlung betreten. Du wirst nach dem Schliessen vom "
"Popup zur Bezahlseite weitergeleitet." "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." msgid "Thank you for the order."
msgstr "Danke für Deine Bestellung." msgstr "Danke für Deine Bestellung."
@ -539,6 +554,28 @@ msgstr ""
"Deine VM ist gleich bereit. Wir senden Dir eine Bestätigungsemail, sobald Du " "Deine VM ist gleich bereit. Wir senden Dir eine Bestätigungsemail, sobald Du "
"auf sie zugreifen kannst." "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" #~ msgid "Pricing"
#~ msgstr "Preise" #~ msgstr "Preise"

View file

@ -159,3 +159,24 @@ footer .dcl-link-separator::before {
margin-top: 10px; margin-top: 10px;
margin-bottom: 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;
}

View file

@ -101,93 +101,55 @@
</div> </div>
<div class="dcl-payment-box"> <div class="dcl-payment-box">
<div class="dcl-payment-section"> <div class="dcl-payment-section">
{% with card_list_len=cards_list|length %}
<h3><b>{%trans "Credit Card"%}</b></h3> <h3><b>{%trans "Credit Card"%}</b></h3>
<hr class="top-hr"> <hr class="top-hr">
<p> <p>
{% 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 <a href="https://stripe.com" target="_blank">Stripe</a> 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 <a href="https://stripe.com" target="_blank">Stripe</a> for payment and do not store your information in our database.{% endblocktrans %} {% blocktrans %}Please fill in your credit card information below. We are using <a href="https://stripe.com" target="_blank">Stripe</a> for payment and do not store your information in our database.{% endblocktrans %}
{% endif %}
</p> </p>
<div> <div>
{% if credit_card_data.last4 %} {% for card in cards_list %}
<form role="form" id="payment-form-with-creditcard" novalidate> <div class="credit-card-info">
<h5 class="billing-head">Credit Card</h5> <div class="col-xs-6 no-padding">
<h5 class="membership-lead">Last 4: *****{{credit_card_data.last4}}</h5> <h5 class="billing-head">{% trans "Credit Card" %}</h5>
<h5 class="membership-lead">Type: {{credit_card_data.cc_brand}}</h5> <h5 class="membership-lead">{% trans "Last" %} 4: ***** {{card.last4}}</h5>
<input type="hidden" name="credit_card_needed" value="false"/> <h5 class="membership-lead">{% trans "Type" %}: {{card.brand}}</h5>
</form> </div>
{% if not messages and not form.non_field_errors %} <div class="col-xs-6 text-right align-bottom">
<p class="card-warning-content card-warning-addtional-margin"> <a class="btn choice-btn choice-btn-faded" href="#" data-id_card="{{card.id}}">{% trans "SELECT" %}</a>
{% trans "You are not making any payment yet. After submitting your card information, you will be taken to the Confirm Order Page." %} </div>
</p> </div>
{% endif %}
<div id='payment_error'>
{% for message in messages %}
{% if 'failed_payment' or 'make_charge_error' in message.tags %}
<ul class="list-unstyled">
<li>
<p class="card-warning-content card-warning-error">{{ message|safe }}</p>
</li>
</ul>
{% endif %}
{% endfor %} {% endfor %}
{% for error in form.non_field_errors %} {% if card_list_len > 0 %}
<p class="card-warning-content card-warning-error"> <div class="new-card-head">
{{ error|escape }}
</p>
{% endfor %}
</div>
<div class="text-right">
<button id="payment_button_with_creditcard" class="btn btn-vm-contact" type="submit">{%trans "SUBMIT" %}</button>
</div>
{% else %}
<form action="" id="payment-form-new" method="POST">
<input type="hidden" name="token"/>
<div class="group">
<div class="credit-card-goup">
<div class="card-element card-number-element">
<label>{%trans "Card Number" %}</label>
<div id="card-number-element" class="field my-input"></div>
</div>
<div class="row"> <div class="row">
<div class="col-xs-5 card-element card-expiry-element"> <div class="col-xs-6">
<label>{%trans "Expiry Date" %}</label> <h4>{% trans "Add a new credit card" %}</h4>
<div id="card-expiry-element" class="field my-input"></div>
</div> </div>
<div class="col-xs-3 col-xs-offset-4 card-element card-cvc-element"> <div class="col-xs-6 text-right new-card-button-margin">
<label>{%trans "CVC" %}</label> <button data-toggle="collapse" data-target="#newcard" class="btn choice-btn">
<div id="card-cvc-element" class="field my-input"></div> <span class="fa fa-plus"></span>&nbsp;&nbsp;{% trans "NEW CARD" %}
</div> </button>
</div>
<div class="card-element brand">
<label>{%trans "Card Type" %}</label>
<i class="pf pf-credit-card" id="brand-icon"></i>
</div> </div>
</div> </div>
</div> </div>
<div id="card-errors"></div> <div id="newcard" class="collapse">
{% if not messages and not form.non_field_errors %} <hr class="thick-hr">
<p class="card-warning-content"> <div class="card-details-box">
{% trans "You are not making any payment yet. After placing your order, you will be taken to the Submit Payment Page." %} <h3>{%trans "New Credit Card" %}</h3>
</p> <hr>
{% endif %} {% include "hosting/includes/_card_input.html" %}
<div id='payment_error'>
{% for message in messages %}
{% if 'failed_payment' in message.tags or 'make_charge_error' in message.tags or 'error' in message.tags %}
<ul class="list-unstyled">
<li><p class="card-warning-content card-warning-error">{{ message|safe }}</p></li>
</ul>
{% endif %}
{% endfor %}
</div> </div>
<div class="text-right">
<button class="btn btn-vm-contact btn-wide" type="submit">{%trans "SUBMIT" %}</button>
</div> </div>
{% else%}
<div style="display:none;"> {% include "hosting/includes/_card_input.html" %}
<p class="payment-errors"></p>
</div>
</form>
{% endif %} {% endif %}
</div> </div>
{% endwith %}
</div> </div>
</div> </div>
</div> </div>
@ -207,13 +169,4 @@
})(); })();
</script> </script>
{%endif%} {%endif%}
{% if credit_card_data.last4 and credit_card_data.cc_brand %}
<script type="text/javascript">
(function () {
window.hasCreditcard = true;
})();
</script>
{%endif%}
{%endblock%} {%endblock%}

View file

@ -86,8 +86,7 @@ class CeleryTaskTestCase(TestCase):
token=self.token token=self.token
) )
card_details = self.stripe_utils.get_card_details( card_details = self.stripe_utils.get_card_details(
stripe_customer.stripe_id, stripe_customer.stripe_id
self.token
) )
card_details_dict = card_details.get('error') card_details_dict = card_details.get('error')
self.assertEquals(card_details_dict, None) self.assertEquals(card_details_dict, None)

View file

@ -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) create_vm_task.delay(vm_template_id, user, specs, template, order.id)
for session_var in ['specs', 'template', 'billing_address', for session_var in ['specs', 'template', 'billing_address',
'billing_address_data', 'billing_address_data', 'card_id',
'token', 'customer']: 'token', 'customer']:
if session_var in request.session: if session_var in request.session:
del request.session[session_var] del request.session[session_var]

View file

@ -13,7 +13,7 @@ from django.views.decorators.cache import cache_control
from django.views.generic import FormView, CreateView, DetailView from django.views.generic import FormView, CreateView, DetailView
from hosting.forms import HostingUserLoginForm from hosting.forms import HostingUserLoginForm
from hosting.models import HostingOrder from hosting.models import HostingOrder, UserCardDetail
from membership.models import CustomUser, StripeCustomer from membership.models import CustomUser, StripeCustomer
from opennebula_api.serializers import VMTemplateSerializer from opennebula_api.serializers import VMTemplateSerializer
from utils.forms import BillingAddressForm, BillingAddressFormSignup from utils.forms import BillingAddressForm, BillingAddressFormSignup
@ -222,19 +222,15 @@ class PaymentOrderView(FormView):
billing_address_form = BillingAddressForm( billing_address_form = BillingAddressForm(
instance=self.request.user.billing_addresses.first() instance=self.request.user.billing_addresses.first()
) )
# Get user last order user = self.request.user
last_hosting_order = HostingOrder.objects.filter( if hasattr(user, 'stripecustomer'):
customer__user=self.request.user stripe_customer = user.stripecustomer
).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: else:
context['credit_card_data'] = None stripe_customer = None
cards_list = UserCardDetail.get_all_cards_list(
stripe_customer=stripe_customer
)
context.update({'cards_list': cards_list})
else: else:
billing_address_form = BillingAddressFormSignup( billing_address_form = BillingAddressFormSignup(
initial=billing_address_data initial=billing_address_data
@ -286,14 +282,42 @@ class PaymentOrderView(FormView):
) )
if address_form.is_valid(): if address_form.is_valid():
token = address_form.cleaned_data.get('token') 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(): if request.user.is_authenticated():
this_user = { this_user = {
'email': request.user.email, 'email': request.user.email,
'name': request.user.name 'name': request.user.name
} }
customer = StripeCustomer.get_or_create( customer = StripeCustomer.get_or_create(
email=this_user.get('email'), email=this_user.get('email'), token=token
token=token) )
else: else:
user_email = address_form.cleaned_data.get('email') user_email = address_form.cleaned_data.get('email')
user_name = address_form.cleaned_data.get('name') user_name = address_form.cleaned_data.get('name')
@ -341,7 +365,6 @@ class PaymentOrderView(FormView):
billing_address_form=address_form billing_address_form=address_form
) )
) )
request.session['token'] = token
if type(customer) is StripeCustomer: if type(customer) is StripeCustomer:
request.session['customer'] = customer.stripe_id request.session['customer'] = customer.stripe_id
else: else:
@ -362,32 +385,34 @@ class OrderConfirmationView(DetailView):
@cache_control(no_cache=True, must_revalidate=True, no_store=True) @cache_control(no_cache=True, must_revalidate=True, no_store=True)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
context = {}
if 'specs' not in request.session or 'user' not in request.session: if 'specs' not in request.session or 'user' not in request.session:
return HttpResponseRedirect(reverse('datacenterlight:index')) return HttpResponseRedirect(reverse('datacenterlight:index'))
if 'token' not in request.session: if 'token' in self.request.session:
return HttpResponseRedirect(reverse('datacenterlight:payment')) token = self.request.session['token']
stripe_api_cus_id = request.session.get('customer')
stripe_utils = StripeUtils() stripe_utils = StripeUtils()
card_details = stripe_utils.get_card_details(stripe_api_cus_id, card_details = stripe_utils.get_cards_details_from_token(
request.session.get( token
'token')) )
if not card_details.get('response_object'): if not card_details.get('response_object'):
msg = card_details.get('error') return HttpResponseRedirect(reverse('hosting:payment'))
messages.add_message(self.request, messages.ERROR, msg, card_details_response = card_details['response_object']
extra_tags='failed_payment') context['cc_last4'] = card_details_response['last4']
return HttpResponseRedirect( context['cc_brand'] = card_details_response['brand']
reverse('datacenterlight:payment') + '#payment_error') else:
context = { 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'), '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'), 'vm': request.session.get('specs'),
'page_header_text': _('Confirm Order'), 'page_header_text': _('Confirm Order'),
'billing_address_data': ( 'billing_address_data': (
request.session.get('billing_address_data') request.session.get('billing_address_data')
), ),
'cms_integration': get_cms_integration('default'), 'cms_integration': get_cms_integration('default'),
} })
return render(request, self.template_name, context) return render(request, self.template_name, context)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
@ -397,13 +422,75 @@ class OrderConfirmationView(DetailView):
stripe_api_cus_id = request.session.get('customer') stripe_api_cus_id = request.session.get('customer')
vm_template_id = template.get('id', 1) vm_template_id = template.get('id', 1)
stripe_utils = StripeUtils() stripe_utils = StripeUtils()
card_details = stripe_utils.get_card_details(stripe_api_cus_id,
request.session.get( if 'token' in request.session:
'token')) card_details = stripe_utils.get_cards_details_from_token(
request.session.get('token')
)
if not card_details.get('response_object'): if not card_details.get('response_object'):
msg = card_details.get('error') msg = card_details.get('error')
messages.add_message(self.request, messages.ERROR, msg, messages.add_message(self.request, messages.ERROR, msg,
extra_tags='failed_payment') 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 = { response = {
'status': False, 'status': False,
'redirect': "{url}#{section}".format( 'redirect': "{url}#{section}".format(
@ -417,7 +504,6 @@ class OrderConfirmationView(DetailView):
} }
return JsonResponse(response) return JsonResponse(response)
card_details_dict = card_details.get('response_object')
cpu = specs.get('cpu') cpu = specs.get('cpu')
memory = specs.get('memory') memory = specs.get('memory')
disk_size = specs.get('disk_size') disk_size = specs.get('disk_size')
@ -442,6 +528,12 @@ class OrderConfirmationView(DetailView):
# Check if the subscription was approved and is active # Check if the subscription was approved and is active
if (stripe_subscription_obj is None if (stripe_subscription_obj is None
or stripe_subscription_obj.status != 'active'): 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') msg = subscription_result.get('error')
messages.add_message(self.request, messages.ERROR, msg, messages.add_message(self.request, messages.ERROR, msg,
extra_tags='failed_payment') extra_tags='failed_payment')
@ -496,12 +588,36 @@ class OrderConfirmationView(DetailView):
stripe_customer_id = request.user.stripecustomer.id stripe_customer_id = request.user.stripecustomer.id
custom_user = request.user 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 # Save billing address
billing_address_data = request.session.get('billing_address_data') billing_address_data = request.session.get('billing_address_data')
logger.debug('billing_address_data is {}'.format(billing_address_data)) logger.debug('billing_address_data is {}'.format(billing_address_data))
billing_address_data.update({ billing_address_data.update({
'user': custom_user.id 'user': custom_user.id
}) })
user = { user = {
'name': custom_user.name, 'name': custom_user.name,
'email': custom_user.email, 'email': custom_user.email,

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -274,6 +274,29 @@ msgstr ""
msgid "You can always order a new VM by following the link below." msgid "You can always order a new VM by following the link below."
msgstr "" 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" msgid "Toggle navigation"
msgstr "Umschalten" msgstr "Umschalten"
@ -439,6 +462,17 @@ msgstr "wird an der Kasse angewendet"
msgid "Billing Address" msgid "Billing Address"
msgstr "Rechnungsadresse" msgstr "Rechnungsadresse"
msgid ""
"Please select one of the cards that you used before or fill in your credit "
"card information below. We are using <a href=\"https://stripe.com\" target="
"\"_blank\">Stripe</a> 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 "
"<a href=\"https://stripe.com\" target=\"_blank\">Stripe</a> abgewickelt. "
"Wir speichern Deine Kreditkartendetails nicht in unserer Datenbank."
msgid "" msgid ""
"Please fill in your credit card information below. We are using <a href=" "Please fill in your credit card information below. We are using <a href="
"\"https://stripe.com\" target=\"_blank\">Stripe</a> for payment and do not " "\"https://stripe.com\" target=\"_blank\">Stripe</a> for payment and do not "
@ -448,28 +482,24 @@ msgstr ""
"\"https://stripe.com\" target=\"_blank\">Stripe</a> für die Bezahlung und " "\"https://stripe.com\" target=\"_blank\">Stripe</a> für die Bezahlung und "
"speichern keine Informationen in unserer Datenbank." "speichern keine Informationen in unserer Datenbank."
msgid "" msgid "Last"
"You are not making any payment yet. After submitting your card information, " msgstr "Letzten"
"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 "SUBMIT" msgid "Type"
msgstr "ABSENDEN"
msgid "Card Number"
msgstr "Kreditkartennummer"
msgid "Expiry Date"
msgstr "Ablaufdatum"
msgid "CVC"
msgstr ""
msgid "Card Type"
msgstr "Kartentyp" 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" msgid "Processing"
msgstr "Weiter" msgstr "Weiter"
@ -483,13 +513,22 @@ msgid "Password reset"
msgstr "Passwort zurücksetzen" msgstr "Passwort zurücksetzen"
msgid "UPDATE" msgid "UPDATE"
msgstr "" msgstr "AKTUALISIEREN"
msgid "Last" msgid "REMOVE CARD"
msgstr "" msgstr "KARTE ENTFERNEN"
msgid "Type" msgid "Remove Card"
msgstr "Kartentyp" 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" msgid "No Credit Cards Added"
msgstr "Es wurde keine Kreditkarte hinzugefügt" msgstr "Es wurde keine Kreditkarte hinzugefügt"
@ -534,10 +573,7 @@ msgid "Public Key"
msgstr "" msgstr ""
msgid "Private Key" msgid "Private Key"
msgstr "" msgstr "Privater Schlüssel"
msgid "Delete"
msgstr "Löschen"
msgid "Delete SSH Key" msgid "Delete SSH Key"
msgstr "SSH Key löschen" 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." msgid "The reset password link is no longer valid."
msgstr "Der Link zum Zurücksetzen Deines Passwortes ist nicht mehr gültig." 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" msgid "Invalid credit card"
msgstr "Ungültige Kreditkarte" msgstr "Ungültige Kreditkarte"
@ -807,15 +873,6 @@ msgstr ""
#~ msgid "Notifications " #~ msgid "Notifications "
#~ msgstr "Benachrichtigungen" #~ 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." #~ msgid "You are not making any payment here."
#~ msgstr "Es wird noch keine Bezahlung vorgenommen" #~ msgstr "Es wird noch keine Bezahlung vorgenommen"

View file

@ -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)))

View file

@ -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),
),
]

View file

@ -1,16 +1,17 @@
import os
import logging 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.db import models
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
from Crypto.PublicKey import RSA
from datacenterlight.models import VMPricing, VMTemplate from datacenterlight.models import VMPricing, VMTemplate
from membership.models import StripeCustomer, CustomUser from membership.models import StripeCustomer, CustomUser
from utils.models import BillingAddress from utils.models import BillingAddress
from utils.mixins import AssignPermissionsMixin from utils.mixins import AssignPermissionsMixin
from utils.stripe_utils import StripeUtils
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -205,3 +206,156 @@ class VMDetail(models.Model):
months = relativedelta(end_date, self.created_at).months or 1 months = relativedelta(end_date, self.created_at).months or 1
end_date = self.created_at + relativedelta(months=months, days=-1) end_date = self.created_at + relativedelta(months=months, days=-1)
return end_date 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

View file

@ -23,6 +23,13 @@
margin: 0 auto; margin: 0 auto;
max-width: 1120px; max-width: 1120px;
} }
.container-table{
margin-top: 35px;
overflow-y: hidden;
}
.container-table table{
overflow-y: auto;
}
.borderless td { .borderless td {
border: none !important; border: none !important;
} }
@ -35,6 +42,19 @@
color: transparent; 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{ .table>tbody>tr>td{
vertical-align: middle; vertical-align: middle;
} }
@ -274,6 +294,26 @@
font-size: 16px; 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 { .credit-card-form {
max-width: 360px; max-width: 360px;
} }
@ -293,8 +333,15 @@
text-decoration: none; text-decoration: none;
} }
.settings-container .credit-card-details-opt { .settings-container .new-card-head {
padding-top: 15px; 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 { .caps-link .svg-img {
@ -313,7 +360,11 @@
.settings-container .btn-vm-contact { .settings-container .btn-vm-contact {
font-weight: 600; font-weight: 600;
font-size: 13px; font-size: 13px;
/* padding: 4px 15px; */ }
.settings-container .choice-btn {
letter-spacing: 2px;
min-width: 127px;
} }
.btn-wide { .btn-wide {
@ -355,6 +406,15 @@
fill: #999; fill: #999;
} }
.card-details-box {
border: 1px solid #eee;
padding: 5px 25px 25px;
}
.thick-hr {
border-top: 5px solid #eee;
}
.locale_date { .locale_date {
opacity: 0; opacity: 0;
} }
@ -371,3 +431,12 @@
margin-top: 10px; margin-top: 10px;
margin-bottom: 10px; margin-bottom: 10px;
} }
.new-card-head {
margin-top: 10px;
}
.new-card-button-margin button{
margin-top: 5px;
margin-bottom: 5px;
}

View file

@ -269,7 +269,7 @@
border: 2px solid #A3C0E2; border: 2px solid #A3C0E2;
padding: 5px 25px; padding: 5px 25px;
font-size: 12px; font-size: 12px;
letter-spacing: 1.3px; letter-spacing: 2px;
} }
.btn-vm-contact:hover, .btn-vm-contact:focus { .btn-vm-contact:hover, .btn-vm-contact:focus {
background: #fff; background: #fff;

View file

@ -195,5 +195,11 @@ $(document).ready(function () {
$(element).closest('.form-group').append(error); $(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();
});
}); });

View file

@ -0,0 +1,50 @@
{% load i18n %}
<form action="" id="payment-form-new" method="POST">
<input type="hidden" name="token"/>
<input type="hidden" name="id_card" id="id_card" value=""/>
<div class="group">
<div class="credit-card-goup">
<div class="card-element card-number-element">
<label>{%trans "Card Number" %}</label>
<div id="card-number-element" class="field my-input"></div>
</div>
<div class="row">
<div class="col-xs-5 card-element card-expiry-element">
<label>{%trans "Expiry Date" %}</label>
<div id="card-expiry-element" class="field my-input"></div>
</div>
<div class="col-xs-3 col-xs-offset-4 card-element card-cvc-element">
<label>{%trans "CVC" %}</label>
<div id="card-cvc-element" class="field my-input"></div>
</div>
</div>
<div class="card-element brand">
<label>{%trans "Card Type" %}</label>
<i class="pf pf-credit-card" id="brand-icon"></i>
</div>
</div>
</div>
<div id="card-errors"></div>
{% if not messages and not form.non_field_errors %}
<p class="card-warning-content">
{% trans "You are not making any payment yet. After placing your order, you will be taken to the Submit Payment Page." %}
</p>
{% endif %}
<div id='payment_error'>
{% for message in messages %}
{% if 'failed_payment' in message.tags or 'make_charge_error' in message.tags or 'error' in message.tags %}
<ul class="list-unstyled">
<li><p class="card-warning-content card-warning-error">{{ message|safe }}</p></li>
</ul>
{% endif %}
{% endfor %}
</div>
<div class="text-right">
<button class="btn btn-vm-contact btn-wide" type="submit" name="payment-form">{%trans "SUBMIT" %}</button>
</div>
<div style="display:none;">
<p class="payment-errors"></p>
</div>
</form>

View file

@ -105,114 +105,65 @@
<h3><b>{%trans "Billing Address"%}</b></h3> <h3><b>{%trans "Billing Address"%}</b></h3>
<hr> <hr>
<form role="form" id="billing-form" method="post" action="" novalidate> <form role="form" id="billing-form" method="post" action="" novalidate>
{% for field in form %}
{% csrf_token %} {% csrf_token %}
{% for field in form %}
{% bootstrap_field field show_label=False type='fields'%} {% bootstrap_field field show_label=False type='fields'%}
{% endfor %} {% endfor %}
</form> </form>
</div> </div>
</div> </div>
<div class="col-sm-7 col-md-6"> <div class="col-xs-12 col-sm-7 col-md-6 creditcard-box dcl-creditcard">
<div class="creditcard-box dcl-creditcard"> {% with card_list_len=cards_list|length %}
<h3><b>{%trans "Credit Card"%}</b></h3> <h3><b>{%trans "Credit Card"%}</b></h3>
<hr> <hr>
<div> <div>
<p> <p>
{% 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 <a href="https://stripe.com" target="_blank">Stripe</a> 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 <a href="https://stripe.com" target="_blank">Stripe</a> for payment and do not store your information in our database.{% endblocktrans %} {% blocktrans %}Please fill in your credit card information below. We are using <a href="https://stripe.com" target="_blank">Stripe</a> for payment and do not store your information in our database.{% endblocktrans %}
{% endif %}
</p> </p>
<div> <div>
{% if credit_card_data.last4 %} {% for card in cards_list %}
<form role="form" id="payment-form-with-creditcard" novalidate> <div class="credit-card-info">
<h5 class="billing-head">Credit Card</h5> <div class="col-xs-6 no-padding">
<h5 class="membership-lead">Last 4: *****{{credit_card_data.last4}}</h5> <h5 class="billing-head">{% trans "Credit Card" %}</h5>
<h5 class="membership-lead">Type: {{credit_card_data.cc_brand}}</h5> <h5 class="membership-lead">{% trans "Last" %} 4: ***** {{card.last4}}</h5>
<input type="hidden" name="credit_card_needed" value="false"/> <h5 class="membership-lead">{% trans "Type" %}: {{card.brand}}</h5>
</form> </div>
{% if not messages and not form.non_field_errors %} <div class="col-xs-6 text-right align-bottom">
<p class="card-warning-content card-warning-addtional-margin"> <a class="btn choice-btn choice-btn-faded" href="#" data-id_card="{{card.id}}">{% trans "SELECT" %}</a>
{% trans "You are not making any payment yet. After submitting your card information, you will be taken to the Confirm Order Page." %} </div>
</p> </div>
{% endif %}
<div id='payment_error'>
{% for message in messages %}
{% if 'failed_payment' or 'make_charge_error' in message.tags %}
<ul class="list-unstyled">
<li>
<p class="card-warning-content card-warning-error">{{ message|safe }}</p>
</li>
</ul>
{% endif %}
{% endfor %} {% endfor %}
{% for error in form.non_field_errors %} {% if card_list_len > 0 %}
<p class="card-warning-content card-warning-error"> <div class="new-card-head">
{{ error|escape }}
</p>
{% endfor %}
</div>
<div class="text-right">
<button id="payment_button_with_creditcard" class="btn btn-vm-contact" type="submit">{%trans "SUBMIT" %}</button>
</div>
{% else %}
<form action="" id="payment-form-new" method="POST">
<input type="hidden" name="token"/>
<div class="group">
<div class="credit-card-goup">
<div class="card-element card-number-element">
<label>{%trans "Card Number" %}</label>
<div id="card-number-element" class="field my-input"></div>
</div>
<div class="row"> <div class="row">
<div class="col-xs-5 card-element card-expiry-element"> <div class="col-xs-6">
<label>{%trans "Expiry Date" %}</label> <h4>{% trans "Add a new credit card" %}</h4>
<div id="card-expiry-element" class="field my-input"></div>
</div> </div>
<div class="col-xs-3 col-xs-offset-4 card-element card-cvc-element"> <div class="col-xs-6 text-right">
<label>{%trans "CVC" %}</label> <button data-toggle="collapse" data-target="#newcard" class="btn choice-btn">
<div id="card-cvc-element" class="field my-input"></div> <span class="fa fa-plus"></span>&nbsp;&nbsp;{% trans "NEW CARD" %}
</div> </button>
</div>
<div class="card-element brand">
<label>{%trans "Card Type" %}</label>
<i class="pf pf-credit-card" id="brand-icon"></i>
</div> </div>
</div> </div>
</div> </div>
<div id="card-errors"></div> <div id="newcard" class="collapse">
{% if not messages and not form.non_field_errors %} <hr class="thick-hr">
<p class="card-warning-content"> <div class="card-details-box">
{% trans "You are not making any payment yet. After submitting your card information, you will be taken to the Confirm Order Page." %} <h3>{%trans "New Credit Card" %}</h3>
</p> <hr>
{% endif %} {% include "hosting/includes/_card_input.html" %}
<div id='payment_error'>
{% for message in messages %}
{% if 'failed_payment' or 'make_charge_error' in message.tags %}
<ul class="list-unstyled">
<li>
<p class="card-warning-content card-warning-error">{{ message|safe }}</p>
</li>
</ul>
{% endif %}
{% endfor %}
{% for error in form.non_field_errors %}
<p class="card-warning-content card-warning-error">
{{ error|escape }}
</p>
{% endfor %}
</div>
<div class="text-right">
<button class="btn btn-vm-contact btn-wide" type="submit">{%trans "SUBMIT" %}</button>
</div> </div>
</div> </div>
{% else%}
<div style="display:none;"> {% include "hosting/includes/_card_input.html" %}
<p class="payment-errors"></p>
</div>
</form>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> {% endwith %}
</div> </div>
</div> </div>
</div> </div>
@ -232,7 +183,7 @@
})(); })();
</script> </script>
{%endif%} {%endif%}
{% comment "Looks as if no more used. To test..." %}
{% if credit_card_data.last4 and credit_card_data.cc_brand %} {% if credit_card_data.last4 and credit_card_data.cc_brand %}
<script type="text/javascript"> <script type="text/javascript">
(function () { (function () {
@ -240,5 +191,5 @@
})(); })();
</script> </script>
{%endif%} {%endif%}
{% endcomment %}
{%endblock%} {%endblock%}

View file

@ -7,6 +7,7 @@
{% block content %} {% block content %}
<div class="dashboard-container wide"> <div class="dashboard-container wide">
{% include 'hosting/includes/_messages.html' %}
<div class="dashboard-container-head"> <div class="dashboard-container-head">
<h1 class="dashboard-title-thin"><img src="{% static 'hosting/img/dashboard_settings.svg' %}" class="un-icon wide"> {% trans "My Settings" %}</h1> <h1 class="dashboard-title-thin"><img src="{% static 'hosting/img/dashboard_settings.svg' %}" class="un-icon wide"> {% trans "My Settings" %}</h1>
</div> </div>
@ -14,7 +15,7 @@
<div class="settings-container"> <div class="settings-container">
<div class="row"> <div class="row">
<div class="col-sm-5 col-md-6 billing dcl-billing"> <div class="col-sm-5 col-md-6 billing dcl-billing">
<h3>{%trans "Billing Address"%}</h3> <h3>{%trans "Billing Address" %}</h3>
<hr> <hr>
<form role="form" id="billing-form" method="post" action="" novalidate> <form role="form" id="billing-form" method="post" action="" novalidate>
{% for field in form %} {% for field in form %}
@ -22,108 +23,97 @@
{% bootstrap_field field show_label=False type='fields' bound_css_class='' %} {% bootstrap_field field show_label=False type='fields' bound_css_class='' %}
{% endfor %} {% endfor %}
<div class="form-group text-right"> <div class="form-group text-right">
<button type="submit" class="btn btn-vm-contact btn-wide">{% trans "UPDATE" %}</button> <button type="submit" class="btn btn-vm-contact btn-wide" name="billing-form">{% trans "UPDATE" %}</button>
</div> </div>
</form> </form>
</div> </div>
<div class="col-sm-7 col-md-6 creditcard-box dcl-creditcard"> <div class="col-sm-7 col-md-6 creditcard-box dcl-creditcard">
<h3>{%trans "Credit Card"%}</h3> <h3>{%trans "Credit Card" %}</h3>
<hr> <hr>
<div> <div>
{% if credit_card_data.last4 %} {% with card_list_len=cards_list|length %}
{% for card in cards_list %}
<div class="credit-card-details"> <div class="credit-card-details">
<h5 class="billing-head">{% trans "Credit Card" %}</h5> <h5 class="billing-head">{% trans "Credit Card" %}</h5>
<h5 class="membership-lead">{% trans "Last" %} 4: *****{{credit_card_data.last4}}</h5> <h5 class="membership-lead">{% trans "Last" %} 4: ***** {{card.last4}}</h5>
<h5 class="membership-lead">{% trans "Type" %}: {{credit_card_data.cc_brand}}</h5> <h5 class="membership-lead">{% trans "Type" %}: {{card.brand}}</h5>
{% comment %}
<div class="credit-card-details-opt"> <div class="credit-card-details-opt">
<div class="row"> <div class="row">
{% if card_list_len > 1 %}
<div class="col-xs-6"> <div class="col-xs-6">
<a class="caps-link" href=""><img src="{% static 'hosting/img/delete.svg' %}" class="svg-img">{% trans "REMOVE CARD" %}</a> <a class="caps-link" href="" data-toggle="modal" data-target="#Modal{{ card.id }}"><img src="{% static 'hosting/img/delete.svg' %}" class="svg-img">{% trans "REMOVE CARD" %}</a>
<div class="modal fade" id="Modal{{card.id }}" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Confirm"><span aria-hidden="true">&times;</span></button>
</div> </div>
<div class="modal-body">
<div class="modal-icon"><i class="fa fa-trash" aria-hidden="true"></i></div>
<h4 class="modal-title" id="ModalLabel">{% trans "Remove Card"%}</h4>
<div class="modal-text">
<p>{% trans "Do you want to remove this associated card?"%}</p>
</div>
<form method="post" action="{% url 'hosting:delete_card' card.id %}">
{% csrf_token %}
<div class="modal-footer">
<button type="submit" class="btn btn-danger btn-wide" name="delete_card">{% trans "Delete"%}</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<div class="col-xs-6 text-right"> <div class="col-xs-6 text-right">
<a class="btn btn-vm-contact" href="">{% trans "EDIT CARD" %}</a> {% if card.preferred %}
</div> {% trans "DEFAULT" %}
</div>
</div>
{% endcomment %}
</div>
{% else %} {% else %}
<form method="post" action="">
{% csrf_token %}
<input type="hidden" name="card" value="{{card.id}}">
<a class="btn choice-btn choice-btn-faded" href="#" onclick="$(this).closest('form').submit()">{% trans "SELECT" %}</a>
</form>
{% endif %}
</div>
</div>
</div>
</div>
{% empty %}
<div class="no-cards"> <div class="no-cards">
<h4>{% trans "No Credit Cards Added" %}</h4> <h4>{% trans "No Credit Cards Added" %}</h4>
<p>{% blocktrans %}We are using <a href="https://stripe.com">Stripe</a> for payment and do not store your information in our database.{% endblocktrans %}</p> <p>{% blocktrans %}We are using <a href="https://stripe.com">Stripe</a> for payment and do not store your information in our database.{% endblocktrans %}</p>
</div> </div>
{% comment %}
<h4>{% trans "Add a new Card." %}</h4>
<p style="margin-bottom: 15px;">
{% blocktrans %}Please fill in your credit card information below. We are using <a href="https://stripe.com" target="_blank">Stripe</a> for payment and do not store your information in our database.{% endblocktrans %}
</p>
<form action="" id="payment-form-new" class="credit-card-form" method="POST">
<input type="hidden" name="token"/>
<div class="credit-card-goup">
<div class="card-element card-number-element">
<label>{%trans "Card Number" %}</label>
<div id="card-number-element" class="field my-input"></div>
</div>
<div class="row">
<div class="col-xs-6 col-sm-4 card-element card-expiry-element">
<label>{%trans "Expiry Date" %}</label>
<div id="card-expiry-element" class="field my-input"></div>
</div>
<div class="col-xs-6 col-sm-4 col-sm-offset-4 card-element card-cvc-element">
<label>{%trans "CVC" %}</label>
<div id="card-cvc-element" class="field my-input"></div>
</div>
</div>
<div class="card-element brand">
<label>{%trans "Card Type" %}</label>
<i class="pf pf-credit-card" id="brand-icon"></i>
</div>
</div>
<div id="card-errors" role="alert"></div>
<div>
{% if not messages and not form.non_field_errors %}
<p class="card-warning-content">
{% blocktrans %}You are not making any payment here.{% endblocktrans %}
</p>
{% endif %}
<div id='payment_error'>
{% for message in messages %}
{% if 'failed_payment' or 'make_charge_error' in message.tags %}
<ul class="list-unstyled"><li>
<p class="card-warning-content card-warning-error">{{ message|safe }}</p>
</li></ul>
{% endif %}
{% endfor %} {% endfor %}
{% endwith %}
{% for error in form.non_field_errors %} <div class="new-card-head">
<p class="card-warning-content card-warning-error">
{{ error|escape }}
</p>
{% endfor %}
</div>
<div class="row"> <div class="row">
<div class="col-xs-6 col-xs-offset-6 text-right"> <div class="col-xs-6">
<button class="btn btn-success stripe-payment-btn" type="submit">{%trans "Submit" %} <h4>{% trans "Add a new credit card" %}</h4>
</div>
<div class="col-xs-6 text-right">
<button data-toggle="collapse" data-target="#newcard" class="btn choice-btn">
<span class="fa fa-plus"></span>&nbsp;&nbsp;{% trans "NEW CARD" %}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<div id="newcard" class="collapse">
<div style="display:none;"> <hr class="thick-hr">
<p class="payment-errors"></p> <div class="card-details-box">
<h3>{%trans "New Credit Card" %}</h3>
<hr>
{% include "hosting/includes/_card_input.html" %}
</div>
</div> </div>
</form>
{% endcomment %}
{% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% comment %}
<!-- stripe key data --> <!-- stripe key data -->
{% if stripe_key %} {% if stripe_key %}
{% get_current_language as LANGUAGE_CODE %} {% get_current_language as LANGUAGE_CODE %}
@ -137,13 +127,4 @@
})(); })();
</script> </script>
{%endif%} {%endif%}
{% if credit_card_data.last4 and credit_card_data.cc_brand %}
<script type="text/javascript">
(function () {
window.hasCreditcard = true;
})();
</script>
{%endif%}
{% endcomment %}
{%endblock%} {%endblock%}

View file

@ -43,6 +43,8 @@ urlpatterns = [
name='choice_ssh_keys'), name='choice_ssh_keys'),
url(r'delete_ssh_key/(?P<pk>\d+)/?$', SSHKeyDeleteView.as_view(), url(r'delete_ssh_key/(?P<pk>\d+)/?$', SSHKeyDeleteView.as_view(),
name='delete_ssh_key'), name='delete_ssh_key'),
url(r'delete_card/(?P<pk>\d+)/?$', SettingsView.as_view(),
name='delete_card'),
url(r'create_ssh_key/?$', SSHKeyCreateView.as_view(), url(r'create_ssh_key/?$', SSHKeyCreateView.as_view(),
name='create_ssh_key'), name='create_ssh_key'),
url(r'^notifications/$', NotificationsView.as_view(), url(r'^notifications/$', NotificationsView.as_view(),

View file

@ -15,11 +15,12 @@ from django.http import (
Http404, HttpResponseRedirect, HttpResponse, JsonResponse Http404, HttpResponseRedirect, HttpResponse, JsonResponse
) )
from django.shortcuts import redirect, render 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.http import urlsafe_base64_decode
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import get_language, ugettext_lazy as _ from django.utils.translation import get_language, ugettext_lazy as _
from django.utils.translation import ugettext from django.utils.translation import ugettext
from django.utils.decorators import method_decorator
from django.views.decorators.cache import never_cache from django.views.decorators.cache import never_cache
from django.views.generic import ( from django.views.generic import (
View, CreateView, FormView, ListView, DetailView, DeleteView, 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.models import VMTemplate, VMPricing
from datacenterlight.utils import create_vm, get_cms_integration from datacenterlight.utils import create_vm, get_cms_integration
from hosting.models import UserCardDetail
from membership.models import CustomUser, StripeCustomer from membership.models import CustomUser, StripeCustomer
from opennebula_api.models import OpenNebulaManager from opennebula_api.models import OpenNebulaManager
from opennebula_api.serializers import ( from opennebula_api.serializers import (
@ -43,7 +45,7 @@ from utils.forms import (
BillingAddressForm, PasswordResetRequestForm, UserBillingAddressForm, BillingAddressForm, PasswordResetRequestForm, UserBillingAddressForm,
ResendActivationEmailForm 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.mailer import BaseEmail
from utils.stripe_utils import StripeUtils from utils.stripe_utils import StripeUtils
from utils.tasks import send_plain_email_task from utils.tasks import send_plain_email_task
@ -565,6 +567,7 @@ class SettingsView(LoginRequiredMixin, FormView):
template_name = "hosting/settings.html" template_name = "hosting/settings.html"
login_url = reverse_lazy('hosting:login') login_url = reverse_lazy('hosting:login')
form_class = BillingAddressForm form_class = BillingAddressForm
permission_required = ['view_usercarddetail']
def get_form(self, form_class): def get_form(self, form_class):
""" """
@ -579,25 +582,68 @@ class SettingsView(LoginRequiredMixin, FormView):
context = super(SettingsView, self).get_context_data(**kwargs) context = super(SettingsView, self).get_context_data(**kwargs)
# Get user # Get user
user = self.request.user user = self.request.user
# Get user last order stripe_customer = None
last_hosting_order = HostingOrder.objects.filter( if hasattr(user, 'stripecustomer'):
customer__user=user).last() stripe_customer = user.stripecustomer
# If user has already an hosting order, get the credit card data from cards_list = UserCardDetail.get_all_cards_list(
# it stripe_customer=stripe_customer
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,
})
context.update({ context.update({
'cards_list': cards_list,
'stripe_key': settings.STRIPE_API_PUBLIC_KEY 'stripe_key': settings.STRIPE_API_PUBLIC_KEY
}) })
return context return context
def post(self, request, *args, **kwargs): 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() form = self.get_form()
if form.is_valid(): if form.is_valid():
if 'billing-form' in request.POST:
billing_address_data = form.cleaned_data billing_address_data = form.cleaned_data
billing_address_data.update({ billing_address_data.update({
'user': self.request.user.id 'user': self.request.user.id
@ -606,6 +652,54 @@ class SettingsView(LoginRequiredMixin, FormView):
instance=self.request.user.billing_addresses.first(), instance=self.request.user.billing_addresses.first(),
data=billing_address_data) data=billing_address_data)
billing_address_user_form.save() 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()) return self.render_to_response(self.get_context_data())
else: else:
billing_address_data = form.cleaned_data billing_address_data = form.cleaned_data
@ -638,24 +732,19 @@ class PaymentVMView(LoginRequiredMixin, FormView):
context = super(PaymentVMView, self).get_context_data(**kwargs) context = super(PaymentVMView, self).get_context_data(**kwargs)
# Get user # Get user
user = self.request.user user = self.request.user
if hasattr(user, 'stripecustomer'):
# Get user last order stripe_customer = user.stripecustomer
last_hosting_order = HostingOrder.objects.filter( else:
customer__user=user).last() stripe_customer = None
cards_list = UserCardDetail.get_all_cards_list(
# If user has already an hosting order, get the credit card data from stripe_customer=stripe_customer
# 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,
})
context.update({ context.update({
'stripe_key': settings.STRIPE_API_PUBLIC_KEY, 'stripe_key': settings.STRIPE_API_PUBLIC_KEY,
'vm_pricing': VMPricing.get_vm_pricing_by_name( 'vm_pricing': VMPricing.get_vm_pricing_by_name(
self.request.session.get('specs', {}).get('pricing_name') self.request.session.get('specs', {}).get('pricing_name')
), ),
'cards_list': cards_list,
}) })
return context return context
@ -664,6 +753,10 @@ class PaymentVMView(LoginRequiredMixin, FormView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if 'next' in request.session: if 'next' in request.session:
del request.session['next'] 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()) return self.render_to_response(self.get_context_data())
@method_decorator(decorators) @method_decorator(decorators)
@ -674,9 +767,38 @@ class PaymentVMView(LoginRequiredMixin, FormView):
billing_address_data = form.cleaned_data billing_address_data = form.cleaned_data
token = form.cleaned_data.get('token') token = form.cleaned_data.get('token')
owner = self.request.user owner = self.request.user
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 # Get or create stripe customer
customer = StripeCustomer.get_or_create(email=owner.email, customer = StripeCustomer.get_or_create(
token=token) email=owner.email, token=token
)
if not customer: if not customer:
msg = _("Invalid credit card") msg = _("Invalid credit card")
messages.add_message( messages.add_message(
@ -684,13 +806,12 @@ class PaymentVMView(LoginRequiredMixin, FormView):
extra_tags='make_charge_error') extra_tags='make_charge_error')
return HttpResponseRedirect( return HttpResponseRedirect(
reverse('hosting:payment') + '#payment_error') reverse('hosting:payment') + '#payment_error')
request.session['billing_address_data'] = billing_address_data
request.session['token'] = token request.session['token'] = token
request.session['customer'] = customer.stripe_id request.session['billing_address_data'] = billing_address_data
return HttpResponseRedirect("{url}?{query_params}".format( return HttpResponseRedirect("{url}?{query_params}".format(
url=reverse('hosting:order-confirmation'), url=reverse('hosting:order-confirmation'),
query_params='page=payment')) query_params='page=payment')
)
else: else:
return self.form_invalid(form) return self.form_invalid(form)
@ -723,12 +844,6 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
).get_context_data(**kwargs) ).get_context_data(**kwargs)
obj = self.get_object() obj = self.get_object()
owner = self.request.user 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': if self.request.GET.get('page') == 'payment':
context['page_header_text'] = _('Confirm Order') context['page_header_text'] = _('Confirm Order')
@ -784,8 +899,9 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
context['vm']['price'] = price context['vm']['price'] = price
context['vm']['discount'] = discount context['vm']['discount'] = discount
context['vm']['vat_percent'] = vat_percent context['vm']['vat_percent'] = vat_percent
context['vm']['total_price'] = price + \ context['vm']['total_price'] = (
vat - discount['amount'] price + vat - discount['amount']
)
except WrongIdError: except WrongIdError:
messages.error( messages.error(
self.request, self.request,
@ -800,17 +916,25 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
_('In order to create a VM, you need to create/upload ' _('In order to create a VM, you need to create/upload '
'your SSH KEY first.') '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: else:
# new order, confirm payment # 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['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') context['vm'] = self.request.session.get('specs')
return context return context
@ -821,7 +945,9 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
return HttpResponseRedirect( return HttpResponseRedirect(
reverse('hosting:create_virtual_machine') 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')) return HttpResponseRedirect(reverse('hosting:payment'))
self.object = self.get_object() self.object = self.get_object()
context = self.get_context_data(object=self.object) context = self.get_context_data(object=self.object)
@ -840,24 +966,68 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
def post(self, request): def post(self, request):
template = request.session.get('template') template = request.session.get('template')
specs = request.session.get('specs') specs = request.session.get('specs')
stripe_utils = StripeUtils()
# We assume that if the user is here, his/her StripeCustomer # We assume that if the user is here, his/her StripeCustomer
# object already exists # object already exists
stripe_customer_id = request.user.stripecustomer.id stripe_customer_id = request.user.stripecustomer.id
billing_address_data = request.session.get('billing_address_data') billing_address_data = request.session.get('billing_address_data')
vm_template_id = template.get('id', 1) vm_template_id = template.get('id', 1)
stripe_api_cus_id = self.request.session.get('customer') stripe_api_cus_id = request.user.stripecustomer.stripe_id
# Make stripe charge to a customer if 'token' in self.request.session:
stripe_utils = StripeUtils() card_details = stripe_utils.get_cards_details_from_token(
card_details = stripe_utils.get_card_details(stripe_api_cus_id, request.session['token']
request.session.get( )
'token'))
if not card_details.get('response_object'): if not card_details.get('response_object'):
msg = card_details.get('error') 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, messages.add_message(self.request, messages.ERROR, msg,
extra_tags='failed_payment') extra_tags='failed_payment')
return HttpResponseRedirect( response = {
reverse('datacenterlight:payment') + '#payment_error') 'status': False,
card_details_dict = card_details.get('response_object') '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') cpu = specs.get('cpu')
memory = specs.get('memory') memory = specs.get('memory')
disk_size = specs.get('disk_size') disk_size = specs.get('disk_size')
@ -882,6 +1052,12 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
# Check if the subscription was approved and is active # Check if the subscription was approved and is active
if (stripe_subscription_obj is None or if (stripe_subscription_obj is None or
stripe_subscription_obj.status != 'active'): 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') msg = subscription_result.get('error')
messages.add_message(self.request, messages.ERROR, msg, messages.add_message(self.request, messages.ERROR, msg,
extra_tags='failed_payment') extra_tags='failed_payment')
@ -894,10 +1070,20 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
'msg_body': str( 'msg_body': str(
_('There was a payment related error.' _('There was a payment related error.'
' On close of this popup, you will be redirected back to' ' On close of this popup, you will be redirected back to'
' the payment page.')) ' the payment page.')
)
} }
return JsonResponse(response) 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 = { user = {
'name': self.request.user.name, 'name': self.request.user.name,
'email': self.request.user.email, 'email': self.request.user.email,

View file

@ -222,21 +222,28 @@ class StripeCustomer(models.Model):
# check if user is not in stripe but in database # check if user is not in stripe but in database
customer = stripe_utils.check_customer(stripe_customer.stripe_id, customer = stripe_utils.check_customer(stripe_customer.stripe_id,
stripe_customer.user, token) stripe_customer.user, token)
if "deleted" in customer and customer["deleted"]:
if not customer.sources.data: raise StripeCustomer.DoesNotExist()
stripe_utils.update_customer_token(customer, token)
return stripe_customer return stripe_customer
except StripeCustomer.DoesNotExist: except StripeCustomer.DoesNotExist:
user = CustomUser.objects.get(email=email) user = CustomUser.objects.get(email=email)
stripe_utils = StripeUtils() stripe_utils = StripeUtils()
stripe_data = stripe_utils.create_customer(token, email, user.name) stripe_data = stripe_utils.create_customer(token, email, user.name)
if stripe_data.get('response_object'): if stripe_data.get('response_object'):
stripe_cus_id = stripe_data.get('response_object').get('id') stripe_cus_id = stripe_data.get('response_object').get('id')
if hasattr(user, 'stripecustomer'):
stripe_customer = StripeCustomer.objects. \ # User already had a Stripe account and we are here
create(user=user, stripe_id=stripe_cus_id) # 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 return stripe_customer
else: else:
return None return None

View file

@ -117,6 +117,7 @@ class EditCreditCardForm(forms.Form):
class BillingAddressForm(forms.ModelForm): class BillingAddressForm(forms.ModelForm):
token = forms.CharField(widget=forms.HiddenInput(), required=False) token = forms.CharField(widget=forms.HiddenInput(), required=False)
card = forms.CharField(widget=forms.HiddenInput(), required=False)
class Meta: class Meta:
model = BillingAddress model = BillingAddress
@ -136,6 +137,31 @@ class BillingAddressFormSignup(BillingAddressForm):
email = forms.EmailField(label=_('Email Address')) email = forms.EmailField(label=_('Email Address'))
field_order = ['name', 'email'] 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): class UserBillingAddressForm(forms.ModelForm):
user = forms.ModelChoiceField(queryset=CustomUser.objects.all(), user = forms.ModelChoiceField(queryset=CustomUser.objects.all(),

View file

@ -128,3 +128,23 @@ def get_vm_price_with_vat(cpu, memory, ssd_size, hdd_size=0,
'amount': float(pricing.discount_amount), 'amount': float(pricing.discount_amount),
} }
return float(price), float(vat), float(vat_percent), discount 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]

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -777,11 +777,18 @@ msgstr ""
msgid "Email Address" msgid "Email Address"
msgstr "" msgstr ""
msgid "Street Building"
msgstr ""
msgid "Email" msgid "Email"
msgstr "E-Mail"
msgid ""
"The email {} is already registered with us. Please reset your password and "
"access your account."
msgstr "" 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" msgid "Phone number"
msgstr "Telefon" msgstr "Telefon"

View file

@ -78,6 +78,22 @@ class StripeUtils(object):
customer.source = token customer.source = token
customer.save() 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 @handleStripeError
def update_customer_card(self, customer_id, token): def update_customer_card(self, customer_id, token):
customer = stripe.Customer.retrieve(customer_id) customer = stripe.Customer.retrieve(customer_id)
@ -93,32 +109,47 @@ class StripeUtils(object):
return new_card_data return new_card_data
@handleStripeError @handleStripeError
def get_card_details(self, customer_id, token): def get_card_details(self, customer_id):
customer = stripe.Customer.retrieve(customer_id) customer = stripe.Customer.retrieve(customer_id)
credit_card_raw_data = customer.sources.data.pop() credit_card_raw_data = customer.sources.data.pop()
card_details = { card_details = {
'last4': credit_card_raw_data.last4, '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 return card_details
def check_customer(self, id, user, token): @handleStripeError
customers = self.stripe.Customer.all() def get_cards_details_from_token(self, token):
if not customers.get('data'): stripe_token = stripe.Token.retrieve(token)
customer = self.create_customer(token, user.email, user.name) card_details = {
else: '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: try:
customer = stripe.Customer.retrieve(id) customer = stripe.Customer.retrieve(stripe_cus_api_id)
except stripe.InvalidRequestError: except stripe.InvalidRequestError:
customer = self.create_customer(token, user.email, user.name) customer = self.create_customer(token, user.email, user.name)
user.stripecustomer.stripe_id = customer.get( user.stripecustomer.stripe_id = customer.get(
'response_object').get('id') 'response_object').get('id')
user.stripecustomer.save() user.stripecustomer.save()
if type(customer) is dict:
customer = customer['response_object']
return customer return customer
@handleStripeError @handleStripeError
def get_customer(self, id): def get_customer(self, stripe_api_cus_id):
customer = stripe.Customer.retrieve(id) customer = stripe.Customer.retrieve(stripe_api_cus_id)
# data = customer.get('response_object') # data = customer.get('response_object')
return customer return customer
@ -296,3 +327,15 @@ class StripeUtils(object):
cpu=cpu, cpu=cpu,
memory=memory, memory=memory,
disk_size=disk_size) 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()