List all the credit cards and the ability to add more cards

This commit is contained in:
amalelshihaby 2021-08-12 12:28:19 +02:00
parent 7986b825a7
commit 1c3d3efb3a
22 changed files with 529 additions and 159 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -1,13 +1,3 @@
function setBrandIcon(brand) {
var brandIconElement = document.getElementById('brand-icon');
var pfClass = 'fa-cc-' + brand;
for (var i = brandIconElement.classList.length - 1; i >= 0; i--) {
brandIconElement.classList.remove(brandIconElement.classList[i]);
}
brandIconElement.classList.add('fab');
brandIconElement.classList.add(pfClass);
}
function fetch_pricing() {
var url = '/pricing/' + $('input[name="pricing_name"]').val() + '/calculate/';
var cores = $('#cores').val();
@ -20,8 +10,18 @@ function fetch_pricing() {
dataType: 'json',
success: function (data) {
if (data && data['total']) {
$('#total').text(data['total'] + " CHF");
$('#recurring_price').text(data['recurring_price'] + " CHF");
$('#vat').text(data['vat_amount'] + " CHF");
$('#total').text(data['total'] + " CHF");
var balance = parseFloat($('#balance').data('balance'));
if(data['total'] > balance) {
$('#has-enough-balance').hide();
$('#cards-section').show();
window.cardNumberElement = loadStripe(stripe);
} else {
$('#cards-section').hide();
$('#has-enough-balance').show();
}
}
}
});
@ -43,8 +43,9 @@ function incrementValue(e) {
return false;
};
$(document).ready(function () {
var stripe = Stripe(window.stripeKey);
if ($('#pricing_name') != undefined) {
fetch_pricing();
}
@ -54,72 +55,8 @@ $(document).ready(function () {
$('.fa-minus-circle.left').bind('click', {inc: -1}, incrementValue);
var hasCreditcard = window.hasCreditcard || false;
if (hasCreditcard && window.stripeKey) {
var stripe = Stripe(window.stripeKey);
if (window.pm_id == undefined) {
var element_style = {
fonts: [{
family: 'lato-light',
src: 'url(https://cdn.jsdelivr.net/font-lato/2.0/Lato/Lato-Light.woff) format("woff2")'
}, {
family: 'lato-regular',
src: 'url(https://cdn.jsdelivr.net/font-lato/2.0/Lato/Lato-Regular.woff) format("woff2")'
}
],
locale: window.current_lan
};
var elements = stripe.elements(element_style);
var credit_card_text_style = {
base: {
iconColor: '#666EE8',
color: '#31325F',
lineHeight: '25px',
fontWeight: 300,
fontFamily: "'lato-light', sans-serif",
fontSize: '14px',
'::placeholder': {
color: '#777'
}
},
invalid: {
iconColor: '#eb4d5c',
color: '#eb4d5c',
lineHeight: '25px',
fontWeight: 300,
fontFamily: "'lato-regular', sans-serif",
fontSize: '14px',
'::placeholder': {
color: '#eb4d5c',
fontWeight: 400
}
}
};
var enter_ccard_text = "Enter your credit card number";
if (typeof window.enter_your_card_text !== 'undefined') {
enter_ccard_text = window.enter_your_card_text;
}
var cardNumberElement = elements.create('cardNumber', {
style: credit_card_text_style,
placeholder: enter_ccard_text
});
cardNumberElement.mount('#card-number-element');
var cardExpiryElement = elements.create('cardExpiry', {
style: credit_card_text_style
});
cardExpiryElement.mount('#card-expiry-element');
var cardCvcElement = elements.create('cardCvc', {
style: credit_card_text_style
});
cardCvcElement.mount('#card-cvc-element');
cardNumberElement.on('change', function (event) {
if (event.brand) {
setBrandIcon(event.brand);
}
});
}
if (hasCreditcard) {
window.cardNumberElement = loadStripe(stripe);
}
function submitBillingForm(pmId) {
@ -141,7 +78,7 @@ $(document).ready(function () {
}
stripe.createPaymentMethod({
type: 'card',
card: cardNumberElement,
card: window.cardNumberElement,
})
.then(function(result) {
// Handle result.error or result.paymentMethod
@ -154,7 +91,7 @@ $(document).ready(function () {
stripePMHandler(result.paymentMethod);
}
});
window.card = cardNumberElement;
window.card = window.cardNumberElement;
}
/* Form validation */

View File

@ -0,0 +1,79 @@
function setBrandIcon(brand) {
var brandIconElement = document.getElementById('brand-icon');
var pfClass = 'fa-cc-' + brand;
for (var i = brandIconElement.classList.length - 1; i >= 0; i--) {
brandIconElement.classList.remove(brandIconElement.classList[i]);
}
brandIconElement.classList.add('fab');
brandIconElement.classList.add(pfClass);
};
function loadStripe(stripe) {
var cardNumberElement;
if (stripe) {
var element_style = {
fonts: [{
family: 'lato-light',
src: 'url(https://cdn.jsdelivr.net/font-lato/2.0/Lato/Lato-Light.woff) format("woff2")'
}, {
family: 'lato-regular',
src: 'url(https://cdn.jsdelivr.net/font-lato/2.0/Lato/Lato-Regular.woff) format("woff2")'
}
],
locale: window.current_lan
};
var elements = stripe.elements(element_style);
var credit_card_text_style = {
base: {
iconColor: '#666EE8',
color: '#31325F',
lineHeight: '25px',
fontWeight: 300,
fontFamily: "'lato-light', sans-serif",
fontSize: '14px',
'::placeholder': {
color: '#777'
}
},
invalid: {
iconColor: '#eb4d5c',
color: '#eb4d5c',
lineHeight: '25px',
fontWeight: 300,
fontFamily: "'lato-regular', sans-serif",
fontSize: '14px',
'::placeholder': {
color: '#eb4d5c',
fontWeight: 400
}
}
};
var enter_ccard_text = "Enter your credit card number";
if (typeof window.enter_your_card_text !== 'undefined') {
enter_ccard_text = window.enter_your_card_text;
}
cardNumberElement = elements.create('cardNumber', {
style: credit_card_text_style,
placeholder: enter_ccard_text
});
cardNumberElement.mount('#card-number-element');
var cardExpiryElement = elements.create('cardExpiry', {
style: credit_card_text_style
});
cardExpiryElement.mount('#card-expiry-element');
var cardCvcElement = elements.create('cardCvc', {
style: credit_card_text_style
});
cardCvcElement.mount('#card-cvc-element');
cardNumberElement.on('change', function (event) {
if (event.brand) {
setBrandIcon(event.brand);
}
});
}
return cardNumberElement;
};

View File

@ -55,6 +55,7 @@
crossorigin="anonymous"
></script>
<script src="{% static 'matrixhosting/js/bootstrap.bundle.min.js' %}"></script>
<script src="{% static 'matrixhosting/js/stripe.js' %}"></script>
<!-- Custom JS -->
{% block js_extra %} {% endblock js_extra %}
{% compress js %}

View File

@ -0,0 +1,210 @@
{% extends "matrixhosting/base.html" %}
{% load static i18n compress %}
{% block title %} Payments {% endblock %}
{% block content %}
<div class="container">
<div class="bg-white shadow-md rounded p-4 m-4">
<div class="row">
<div class="col-md-3">
<ul class="nav nav-tabs flex-column" id="myTabVertical" role="tablist">
<li class="nav-item"> <a class="nav-link" id="first-tab" href="{% url 'matrixhosting:billing' %}">Payment Moves</a> </li>
<li class="nav-item"> <a class="nav-link active" id="fourth-tab" href="{% url 'matrixhosting:cards' %}">Payment Methods</a> </li>
</ul>
</div>
<div class="col-md-9">
<div class="tab-content my-3" id="myTabContentVertical">
<div class="tab-pane fade show active" id="cards" role="tabpanel" aria-labelledby="cards">
<div class="row">
<div class="col-12">
<div class="row">
<div class="col-sm-12 col-lg-12">
<div class="featured-box style-3 float-right mb-4">
<div class="featured-box-icon text-17 text-light"> <i class="fas fa-wallet"></i> </div>
<h3 class="text-9 font-weight-400">{{balance}} CHF</h3>
<a href="" data-target="#make-deposit" data-toggle="modal">Make a deposit</a>
</div>
</div>
</div>
</div>
</div>
<hr class="mt-0 mb-4 mx-n8">
<div class="row">
<div class="col-12">
<h3 class="text-4 font-weight-400 mb-4">{% trans "Credit or Debit Cards"%} <span class="text-muted text-3">({% trans "for payments"%})</span></h3>
<hr class="mt-0 mb-2 mx-n8">
<div class="row">
{% for card in object_list %}
<div class="col-12 col-sm-6 col-lg-4 mt-2">
<div class="account-card {% if card.active %}account-card-primary{%endif%} text-white rounded p-3 mb-4 mb-lg-0">
<p class="text-4">XXXX-XXXX-XXXX-{{card.last4}}</p>
<p class="d-flex align-items-center"> <span class="account-card-expire text-uppercase d-inline-block opacity-7 mr-2">Valid<br>
thru<br>
</span> <span class="text-4 opacity-9">{{card.expiry_date|date:"m"}}/{{card.expiry_date|date:"y"}}</span> {% if card.active %}<span class="badge badge-warning text-0 font-weight-500 rounded-pill px-2 ml-auto">{% trans "Primary"%}</span>{%endif%} </p>
<p class="d-flex align-items-center m-0"> <span class="text-uppercase font-weight-500">{{card.card_name}}</span> <img class="ml-auto" src="{% static 'matrixhosting/images/' %}{{card.brand}}.png" alt="visa" title=""> </p>
<div class="account-card-overlay rounded" data-card="{{card.card_id}}"> <a href="#" data-href="{% url 'payments:card_activate'%}" class="activate-btn text-light btn-link mx-2"><span class="mr-1"><i class="fas fa-edit"></i></span>{% trans "Set As Primary"%}</a> <a href="#" data-href="{% url 'stripecreditcard-detail' card.id %}" class="delete-card text-light btn-link mx-2"><span class="mr-1"><i class="fas fa-minus-circle"></i></span>{% trans "Delete"%}</a> </div>
</div>
</div>
{% endfor %}
<div class="col-12 col-sm-6 col-lg-4 mt-2"> <a href="" data-target="#add-new-card-details" data-toggle="modal" class="account-card-new d-flex align-items-center rounded h-100 p-3 mb-4 mb-lg-0">
<p class="w-100 text-center line-height-4 m-0"> <span class="text-3"><i class="fas fa-plus-circle"></i></span> <span class="d-block text-body text-3">Add New Card</span> </p>
</a> </div>
</div>
</div>
</div>
</div>
<div id="make-deposit" class="modal fade" aria-hidden="true" style="display: none;">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title font-weight-400">{% trans "Make a deposit"%}</h5>
<button type="button" class="close font-weight-400" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button>
</div>
<div class="modal-body p-4">
<form id="make_deposit" method="post" action="{% url 'payment-list' %}">
{% csrf_token %}
<input id="type" name="type" type="hidden" value="deposit">
<input id="currency" name="currency" type="hidden" value="CHF">
<div class="form-group">
<label for="amount">{% trans "Amount *"%}</label>
<input id="amount" name="amount" class="form-control" required type="number" min={{min_amount}} value="">
</div>
<div class="form-group">
<label for="notes">{% trans "Notes"%}</label>
<textarea id="notes" name="notes" class="form-control"></textarea>
</div>
<button class="btn btn-primary float-right mt-2" id="deposit-button" type="submit">
{% trans "Confirm"%}
</button>
</form>
</div>
</div>
</div>
</div>
<div id="add-new-card-details" class="modal fade" aria-hidden="true" style="display: none;">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title font-weight-400">{% trans "Add a Card"%}</h5>
<button type="button" class="close font-weight-400" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button>
</div>
<div class="modal-body p-4">
<form id="addCard" method="post" data-secret="{{ client_secret }}">
<div id="card-element"></div>
<div id="card-errors"></div>
<div class="form-group">
<label for="cardNumber">{% trans "Card Number"%}</label>
<div id="card-number-element" class="field my-input form-control"></div>
</div>
<div class="form-row">
<div class="col-lg-6">
<div class="form-group">
<label for="expiryDate">{% trans "Expiry Date"%}</label>
<div id="card-expiry-element" class="field my-input form-control"></div>
</div>
</div>
<div class="col-lg-6">
<div class="form-group">
<label for="cvvNumber">{% trans "CVV"%} <span class="text-info ml-1" data-toggle="tooltip" data-original-title="For Visa/Mastercard, the three-digit CVV number is printed on the signature panel on the back of the card immediately after the card's account number. For American Express, the four-digit CVV number is printed on the front of the card above the card account number."><i class="fas fa-question-circle"></i></span></label>
<div id="card-cvc-element" class="field my-input form-control"></div> </div>
</div>
</div>
<div class="form-group">
<label for="cardHolderName">{% trans "Card Holder Name"%}</label>
<input type="text" class="form-control" data-bv-field="cardholdername" id="cardholder-name" required="" value="" placeholder="Card Holder Name">
</div>
<div class="card-element brand text-6">
<i class="fab fa-credit-card" id="brand-icon"></i>
</div>
<button class="btn btn-primary float-right mt-2" id="card-button" type="submit">{% trans "Add Card"%}</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block js_extra %}
<script src="https://js.stripe.com/v3/"></script>
<script>
var stripe = Stripe('{{ stripe_pk }}');
var setupForm = document.getElementById('addCard');
var clientSecret = setupForm.dataset.secret;
window.cardNumberElement = loadStripe(stripe);
var cardholderName = document.getElementById('cardholder-name');
var messageContainer = document.getElementById('card-errors');
setupForm.addEventListener('submit', function(ev) {
ev.preventDefault();
stripe.confirmCardSetup(
clientSecret,
{
payment_method: {
card: window.cardNumberElement,
billing_details: {
name: cardholderName.value,
},
},
}
).then(function(result) {
if (result.error) {
var message = document.createTextNode('Error:' + result.error.message);
messageContainer.appendChild(message);
} else {
location.reload();
}
});
});
$('.activate-btn').click(function (e) {
e.preventDefault();
var url = $(e.target).data('href');
var card_id = $(e.target).parent().data('card');
$.ajax({
type: 'POST',
url: url,
data: {card_id: card_id, csrfmiddlewaretoken: '{{ csrf_token }}'},
dataType: 'json',
success: function (result) {
location.reload();
}
});
});
$('.delete-card').click(function (e) {
e.preventDefault();
var url = $(e.target).data('href');
var card_id = $(e.target).parent().data('card');
$.ajax({
type: 'DELETE',
url: url,
data: {csrfmiddlewaretoken: '{{ csrf_token }}'},
dataType: 'json',
success: function (result) {
location.reload();
}
});
});
$("#make_deposit").submit(function(e){
var form = $(this);
$('#deposit-button').html('<span class="spinner-border spinner-border-sm mr-2" role="status" aria-hidden="true"></span>Loading...').attr('disabled', true);
$.ajax({
url : form.attr('action'),
type : form.attr('method'),
data : form.serialize(),
success: function(response) {
location.reload();
},
error: function (xhr, ajaxOptions, thrownError) {
alert(xhr.responseJSON);
$('#deposit-button').html("{% trans 'Confirm'%}").attr('disabled', false);
}
});
return false;
});
</script>
{% endblock js_extra %}

View File

@ -24,15 +24,6 @@
</div>
</div>
<div id="card-errors"></div>
<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-primary btn-wide" type="submit" name="payment-form">{%trans "Checkout" %}</button>
</div>

View File

@ -61,7 +61,7 @@
<li><a class="dropdown-item text-center text-primary px-0" href="">See all Notifications</a></li>
</ul>
</li>
<li class="dropdown profile ml-2"> <a class="px-0 dropdown-toggle" href="#"><span class="text-5"><i class="far fa-user"></i></span></a>
<li class="dropdown profile ml-2"> <a class="px-2 dropdown-toggle" href="#"><span class="text-5"><i class="far fa-user"></i></span></a>
<ul class="dropdown-menu">
<li class="text-center text-3 py-2">Hi, {{request.user.username}}</li>
<li class="dropdown-divider mx-n3"></li>
@ -69,7 +69,7 @@
<li><a class="dropdown-item" href=""><i class="fas fa-tachometer-alt"></i>{%trans "Dashboard" %}</a></li>
<li><a class="dropdown-item" href="{% url 'matrix:orders' %}"><i class="fas fa-shopping-cart"></i>{%trans "Orders" %}</a></li>
<li><a class="dropdown-item" href="{% url 'matrix:billing' %}"><i class="fas fa-file-invoice"></i>{%trans "Billing" %}</a></li>
<li><a class="dropdown-item" href=""><i class="fas fa-cloud"></i>{%trans "Instances" %}</a></li>
<li><a class="dropdown-item" href="{% url 'matrix:instances' %}"><i class="fas fa-cloud"></i>{%trans "Instances" %}</a></li>
<li class="dropdown-divider mx-n3"></li>
<li><a class="dropdown-item" href=""><i class="fas fa-life-ring"></i>Need Help?</a></li>
<li><a class="dropdown-item" href="{% url 'account_logout' %}"><i class="fas fa-sign-out-alt"></i>Sign Out</a></li>

View File

@ -0,0 +1,53 @@
{% extends "matrixhosting/base.html" %}
{% load static i18n compress %}
{% block title %} Instances {% endblock %}
{% block content %}
<!-- Page Content -->
{% csrf_token %}
<div class="container">
<div class="row p-1 mt-4">
<div class="col-lg-12 bg-white shadow-sm border border-light rounded py-4 mb-4">
<h3 class="text-5 font-weight-400 d-flex align-items-center px-1 mb-4">{% trans "Instances"%}</h3>
<!-- Title
=============================== -->
<div class="transaction-title py-2 px-1">
<div class="row">
<div class="col-1 col-sm-1 text-center"><span class="">{% trans "ID"%}</span></div>
<div class="col-2 col-sm-2 text-center"><span class="">{% trans "Creation Date"%}</span></div>
<div class="col-3 col-sm-3 d-none d-sm-block text-center">{% trans "Homeserver Domain"%}</div>
<div class="col-3 col-sm-3 text-right">{% trans "WebClient Domain"%}</div>
<div class="col-1 col-sm-1 text-center"><span class="">{% trans "Order"%}</span></div>
<div class="col-2 col-sm-2 text-center">{% trans "Termination Date"%}</div>
</div>
</div>
<!-- Title End -->
<!-- Instances List
=============================== -->
<div class="transaction-list">
{% for instance in object_list %}
<div class="transaction-item px-1 py-4" data-id={{instance.id}}>
<div class="row align-items-center flex-row">
<div class="col-1 col-sm-1 text-center"> <span class="d-block text-3 font-weight-400">#{{instance.id}}</span></div>
<div class="col-2 col-sm-2 text-center"> <span class="d-block text-2 font-weight-300">{{instance.creation_date|date:"Y-m-d"}}</span></div>
<div class="col-3 col-sm-3 d-none d-sm-block text-right text-1"> <span>{{instance.homeserver_domain}}</span> </div>
<div class="col-3 col-sm-3 d-none d-sm-block text-right text-1"> <span>{{instance.webclient_domain}}</span> </div>
<div class="col-1 col-sm-1 text-center text-2"><span class="">#{{instance.order.id}}</span></div>
<div class="col-2 col-sm-2 d-none d-sm-block text-center text-2"> <span class=" text-uppercase">{{order.termination_date|date:"Y-m-d"}}</span> </div>
</div>
</div>
{%endfor%}
</div>
<!-- Instances List End -->
</div>
</div>
</div>
{% endblock %}
{% block js_extra %}
{% endblock %}

View File

@ -180,8 +180,14 @@
</div>
<hr>
</div>
{% if stripe_deposit_amount > 0 %}
<div class="col-sm-12">
By clicking "Place order" you agree to our <a href="">Terms of Service</a> and this plan will charge your account balance with {{pricing.total|floatformat:2}} CHF
By clicking "Confirm order" you agree to charge your active credit card with <strong>{{stripe_deposit_amount}} CHF</strong> to handle the wallet deficit.
<hr>
</div>
{% endif %}
<div class="col-sm-12">
By clicking "Confirm order" you agree to our <a href="">Terms of Service</a> and this plan will charge your account balance with {{pricing.total|floatformat:2}} CHF
</div>
<div class="col-sm-12 order-confirm-btn mt-2 text-right">
<button class="btn choice-btn btn-primary" id="btn-create-vm" type="submit">

View File

@ -183,7 +183,7 @@
<div class="row align-items-center flex-row">
<div class="col col-sm-6"><div class="text-14 text-light"><i class="fas fa-wallet"></i></div></div>
<div class="col col-sm-6 text-right text-2">
<h3 class="text-6 font-weight-400 {% if balance >= 0 %}text-success{%else%}text-danger{%endif%}">{{ balance }} CHF</h3>
<h3 id="balance" class="text-6 font-weight-400 {% if balance >= 0 %}text-success{%else%}text-danger{%endif%}" data-balance='{{ balance }}'>{{ balance }} CHF</h3>
<span class="text-muted text-3 opacity-8">{% trans "Available Balance"%}</span>
</div>
</div>
@ -197,6 +197,7 @@
<p>{% trans "Discount"%} <span class="float-right text-danger"> - {{matrix_vm_pricing.discount_amount}} CHF</span></p>
{% endif %}
<hr>
<p>{% trans "VAT" %}<span class="float-right" id="vat"> {{request.session.pricing.vat_amount}} CHF</span></p>
<p class="text-4 font-weight-500">{% trans "Total To Pay"%}
<small>
({% if matrix_vm_pricing.vat_inclusive %}{%trans "including VAT" %}{% else %}{%trans "excluding VAT" %}{% endif %})
@ -206,8 +207,7 @@
</div>
</div>
<hr class="mt-2 mx-n3">
{% if show_cards %}
<div id="cards-section">
<div id="cards-section" {% if not show_cards %}style="display:none;"{% endif %}>
{% with cards_len=cards|length %}
<p class="text-muted">
{% if cards_len > 0 %}
@ -252,14 +252,14 @@
</div>
{% endwith %}
</div>
{% else %}
<p class="text-muted">
{% blocktrans %}Your wallet has enough balance, Press Continue to fill the VM instance settings.{% endblocktrans %}
</p>
<div class="text-right">
<button id="continue-btn" class="btn btn-primary btn-wide" type="submit">{%trans "Continue" %}</button>
<div id="has-enough-balance" {% if show_cards %}style="display:none;"{% endif %}>
<p class="text-muted">
{% blocktrans %}Your wallet has enough balance, Press Continue to fill the VM instance settings.{% endblocktrans %}
</p>
<div class="text-right">
<button id="continue-btn" class="btn btn-primary btn-wide">{%trans "Continue" %}</button>
</div>
</div>
{% endif %}
</div>
</div>
</div>

View File

@ -10,8 +10,8 @@
<div class="row">
<div class="col-md-3">
<ul class="nav nav-tabs flex-column" id="myTabVertical" role="tablist">
<li class="nav-item"> <a class="nav-link active" id="first-tab" data-toggle="tab" href="#transactions" role="tab" aria-controls="firstTab" aria-selected="true">All Transactions</a> </li>
<li class="nav-item"> <a class="nav-link" id="fourth-tab" data-toggle="tab" href="#cards" role="tab" aria-controls="fourthTab" aria-selected="false">Payment Methods</a> </li>
<li class="nav-item"> <a class="nav-link active" id="first-tab" href="{% url 'matrixhosting:billing' %}">Payment Moves</a> </li>
<li class="nav-item"> <a class="nav-link" id="fourth-tab" href="{% url 'matrixhosting:cards' %}">Payment Methods</a> </li>
</ul>
</div>
<div class="col-md-9">
@ -42,7 +42,7 @@
<div class="col-12 mb-3" id="allFilters">
<div class="custom-control custom-radio custom-control-inline">
<input type="radio" id="allTransactions" name="filter" data-url="{% url 'matrix:billing' %}" class="custom-control-input" value="all" {% if not type %} checked=""{% endif %}>
<label class="custom-control-label" for="allTransactions">{% trans "All Transactions"%}</label>
<label class="custom-control-label" for="allTransactions">{% trans "All"%}</label>
</div>
<div class="custom-control custom-radio custom-control-inline">
<input type="radio" id="withdrawal" name="filter" class="custom-control-input" data-url="{% url 'matrix:billing' %}?type=withdraw" value="withdraw" {% if type == 'withdraw' %} checked=""{% endif %}>
@ -63,7 +63,7 @@
<!-- All Transactions
============================================= -->
<div class="bg-white shadow-sm border border-light rounded py-4 mb-4">
<h3 class="text-5 font-weight-400 d-flex align-items-center px-4 mb-4">{% trans "All Transactions"%}</h3>
<h3 class="text-5 font-weight-400 d-flex align-items-center px-4 mb-4">{% trans "Payment Moves"%}</h3>
<!-- Title
=============================== -->
<div class="transaction-title py-2 px-4">
@ -111,8 +111,6 @@
<!-- All Transactions End -->
</div>
</div>
<div class="tab-pane fade" id="cards" role="tabpanel" aria-labelledby="cards">
</div>
</div>
</div>
</div>

View File

@ -8,10 +8,12 @@ app_name = 'matrixhosting'
urlpatterns = [
path('order/new/', OrderPaymentView.as_view(), name='payment'),
path('order/confirm/', OrderDetailsView.as_view(), name='order_confirmation'),
path('order/confirm/', OrderConfirmationView.as_view(), name='order_confirmation'),
path('order/success/', OrderSuccessView.as_view(), name='order_success'),
path('order/invoice/download', InvoiceDownloadView.as_view(), name='invoice_download'),
path('billing/', PaymentsView.as_view(), name='billing'),
path('billing/cards', CardsView.as_view(), name='cards'),
path('instances/', InstancesView.as_view(), name='instances'),
path('orders/', OrdersView.as_view(), name='orders'),
path('', IndexView.as_view(), name='index'),
]

View File

@ -115,11 +115,6 @@ class OrderPaymentView(FormView):
context.update({'details_form': details_form,
'billing_address_form': billing_address_form})
return self.render_to_response(context)
this_user = {
'email': self.request.user.email,
'username': self.request.user.username
}
address = get_billing_address_for_user(self.request.user)
if address:
form = BillingAddressForm(self.request.POST, instance=address)
@ -131,11 +126,14 @@ class OrderPaymentView(FormView):
self.request.session['billing_address_data'] = billing_address_form.cleaned_data
self.request.session['billing_address_data']['owner'] = self.request.user.id
id_payment_method = self.request.POST.get('id_payment_method', False)
customer_id = uncloud_stripe.get_customer_id_for(self.request.user)
selected_card = False
if id_payment_method and id_payment_method != 'undefined':
uncloud_stripe.attach_payment_method(id_payment_method, customer_id)
uncloud_stripe.attach_payment_method(id_payment_method, self.request.user)
selected_card = StripeCreditCard.objects.filter(card_id=id_payment_method).first()
selected_card.activate()
vat_number = billing_address_form.cleaned_data.get('vat_number').strip()
if vat_number:
customer_id = uncloud_stripe.get_customer_id_for(self.request.user)
validate_result = validate_vat_number(
stripe_customer_id=customer_id,
billing_address_id=billing_address_ins.id
@ -156,24 +154,21 @@ class OrderPaymentView(FormView):
specs['cores'], specs['memory'], specs['storage'], request.session['pricing']['name'],
vat_rate=vat_rate * 100, vat_validation_status = vat_validation_status
)
amount = get_balance_for_user(self.request.user) - decimal.Decimal(pricing["total"])
if (amount < 0):
try:
payment_id = Payment.deposit(request.user, abs(amount), source='stripe')
except CardError as e:
messages.add_message(
self.request, messages.ERROR, e.user_message,
extra_tags='error'
)
return HttpResponseRedirect(
reverse('matrix:payment')
)
self.request.session['pricing'] = pricing
self.request.session['order'] = specs
self.request.session['vat_validation_status'] = vat_validation_status
amount = get_balance_for_user(self.request.user) - decimal.Decimal(pricing["total"])
if (amount < 0 and not selected_card):
messages.add_message(
self.request, messages.ERROR, "You haven't enough balance please select credit card to continue",
extra_tags='error'
)
return HttpResponseRedirect(
reverse('matrix:payment')
)
return HttpResponseRedirect(reverse('matrix:order_confirmation'))
class OrderDetailsView(DetailView):
class OrderConfirmationView(DetailView):
template_name = "matrixhosting/order_confirmation.html"
context_object_name = "order"
model = Order
@ -197,7 +192,12 @@ class OrderDetailsView(DetailView):
if ('order' not in request.session):
return HttpResponseRedirect(reverse('matrix:index'))
elif 'pricing' not in self.request.session or 'vat_validation_status' not in self.request.session:
return HttpResponseRedirect(reverse('matrix:payment'))
return HttpResponseRedirect(reverse('matrix:payment'))
total = self.request.session['pricing']['total']
amount = get_balance_for_user(self.request.user) - decimal.Decimal(total)
if (amount < 0):
context['stripe_deposit_amount'] = max(amount, settings.MIN_PER_TRANSACTION)
return render(request, self.template_name, context)
def post(self, request, *args, **kwargs):
@ -210,19 +210,31 @@ class OrderDetailsView(DetailView):
self.request.session['order']['homeserver_domain'] = domains_form.cleaned_data.get('homeserver_name') + ".matrix.ungleich.cloud"
self.request.session['order']['webclient_domain'] = domains_form.cleaned_data.get('webclient_name') + ".matrix.0co2.cloud"
self.request.session['order']['is_open_registration'] = domains_form.cleaned_data.get('is_open_registration')
order = finalize_order(request, customer,
try:
amount = get_balance_for_user(self.request.user) - decimal.Decimal(total)
if (amount < 0):
Payment.deposit(request.user, abs(max(amount, settings.MIN_PER_TRANSACTION)), source='stripe')
order = finalize_order(request, customer,
billing_address,
total,
PricingPlan.get_by_name(self.request.session['pricing']['name']),
request.session.get('order'))
if order:
bill = Bill.create_next_bill_for_order(order)
self.request.session['bill_id'] = bill.id
payment= Payment.withdraw(owner=request.user, amount=total, notes=f"BILL #{bill.id}")
if payment:
#Close the bill as the payment has been added
bill.close()
return HttpResponseRedirect(reverse('matrix:order_success'))
if order:
bill = Bill.create_next_bill_for_order(order)
self.request.session['bill_id'] = bill.id
payment= Payment.withdraw(owner=request.user, amount=total, notes=f"BILL #{bill.id}")
if payment:
#Close the bill as the payment has been added
bill.close()
return HttpResponseRedirect(reverse('matrix:order_success'))
except CardError as e:
messages.add_message(
self.request, messages.ERROR, e.user_message,
extra_tags='error'
)
return HttpResponseRedirect(
reverse('matrix:order_confirmation')
)
context = self.get_context_data()
context['domains_form'] = domains_form
return self.render_to_response(context)
@ -289,15 +301,17 @@ class OrdersView(ListView):
order.cancel()
return JsonResponse({'message': 'Successfully Cancelled'})
class InstancesView(ListView):
template_name = "matrixhosting/instances.html"
model = VMInstance
class MachineViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = VMInstanceSerializer
permission_classes = [permissions.IsAuthenticated]
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
def get_queryset(self):
return VMInstance.objects.filter(owner=self.request.user)
class PaymentsView(ListView):
template_name = "matrixhosting/payments.html"
model = Payment
@ -316,6 +330,34 @@ class PaymentsView(ListView):
def get_queryset(self):
if self.request.GET.get('type'):
return Payment.objects.filter(owner=self.request.user, type=self.request.GET.get('type'))
return Payment.objects.filter(owner=self.request.user)
return Payment.objects.filter(owner=self.request.user, type=self.request.GET.get('type')).order_by('-timestamp')
return Payment.objects.filter(owner=self.request.user).order_by('-timestamp')
class CardsView(ListView):
template_name = "matrixhosting/cards.html"
model = StripeCreditCard
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
def get_context_data(self, **kwargs):
context = super(CardsView, self).get_context_data(**kwargs)
customer_id = uncloud_stripe.get_customer_id_for(self.request.user)
setup_intent = uncloud_stripe.create_setup_intent(customer_id)
context = super().get_context_data(**kwargs)
context.update({
'balance': get_balance_for_user(self.request.user),
'client_secret': setup_intent.client_secret,
'username': self.request.user.username,
'stripe_pk':uncloud_stripe.public_api_key,
'min_amount': settings.MIN_PER_TRANSACTION
})
return context
def get_queryset(self):
uncloud_stripe.sync_cards_for_user(self.request.user)
return StripeCreditCard.objects.filter(owner=self.request.user).order_by('-active')

View File

@ -15,4 +15,5 @@ GITLAB_YAML_DIR=
GITLAB_PROJECT_ID=
GITLAB_OAUTH_TOKEN=
GITLAB_AUTHOR_EMAIL=
GITLAB_AUTHOR_NAME=
GITLAB_AUTHOR_NAME=
WKHTMLTOPDF_CMD=/usr/bin/wkhtmltopdf

View File

@ -92,7 +92,7 @@ MIDDLEWARE = [
]
ROOT_URLCONF = 'uncloud.urls'
WKHTMLTOPDF_CMD = "/usr/local/bin/wkhtmltopdf"
WKHTMLTOPDF_CMD = env('WKHTMLTOPDF_CMD')
TEMPLATES = [
{
@ -217,6 +217,7 @@ OPENNEBULA_USER_PASS = 'user:password'
STRIPE_KEY=env('STRIPE_KEY')
STRIPE_PUBLIC_KEY=env('STRIPE_PUBLIC_KEY')
BILL_PAYMENT_DELAY = 0
MIN_PER_TRANSACTION = 5
# The django secret key
SECRET_KEY=get_random_secret_key()
@ -279,6 +280,7 @@ REPORT_FORMAT = {
'header_line': False,
}
# Overwrite settings with local settings, if existing
try:
from uncloud.local_settings import *

View File

@ -41,7 +41,6 @@ router.register(r'v2/payment/balance', payviews.BalanceViewSet, basename='paymen
router.register(r'v2/payment/address', payviews.BillingAddressViewSet, basename='billingaddress')
router.register(r'v2/orders', payviews.OrderViewSet, basename='orders')
router.register(r'v2/bill', payviews.BillViewSet, basename='bills')
router.register(r'v2/machines', matrixviews.MachineViewSet, basename='machines')
# Generic helper views that are usually not needed
router.register(r'v2/generic/vat-rate', payviews.VATRateViewSet, basename='vatrate')
@ -62,6 +61,7 @@ urlpatterns = [
path('pricing/<slug:name>/calculate/', payviews.PricingView.as_view(), name='pricing_calculator'),
path('cc/reg/', payviews.RegisterCard.as_view(), name="cc_register"),
path('inbox/notifications/', include(notifications.urls, namespace='notifications')),
path('payments/', include('uncloud_pay.urls', namespace='payments')),
path('', include('matrixhosting.urls', namespace='matrix')),
# path('', uncloudviews.UncloudIndex.as_view(), name="uncloudindex"),
]

View File

@ -58,7 +58,7 @@ class StripeCustomer(models.Model):
class StripeCreditCard(models.Model):
owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
card_name = models.CharField(null=False, max_length=128, default="My credit card")
card_name = models.CharField(null=False, max_length=128, default="")
card_id = models.CharField(null=False, max_length=32)
last4 = models.CharField(null=False, max_length=4)
brand = models.CharField(null=False, max_length=64)
@ -76,6 +76,16 @@ class StripeCreditCard(models.Model):
def __str__(self):
return f"{self.card_name}: {self.brand} {self.last4} ({self.expiry_date})"
def delete(self, **kwargs):
uncloud_pay.stripe.delete_card(self.card_id)
super().delete(**kwargs)
def activate(self):
StripeCreditCard.objects.filter(owner=self.owner, active=True).update(active=False)
self.active = True
self.save()
class Payment(models.Model):
owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
type = models.CharField(max_length=256,
@ -125,6 +135,7 @@ class Payment(models.Model):
return cls.objects.create(owner=owner, type="withdraw", amount=amount,
currency=currency, notes=notes)
# See https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices-enum-types
class RecurringPeriodDefaultChoices(models.IntegerChoices):

View File

@ -2,6 +2,7 @@ from django.contrib.auth import get_user_model
from rest_framework import serializers
from uncloud_auth.serializers import UserSerializer
from django.utils.translation import gettext_lazy as _
from stripe.error import CardError
from .models import *
import uncloud_pay.stripe as uncloud_stripe
@ -25,13 +26,17 @@ class PaymentSerializer(serializers.ModelSerializer):
read_only_fields = [ "external_reference", "source", "timestamp" ]
def validate(self, data):
payment_intent = uncloud_stripe.charge_customer(data['owner'],
data['amount'])
data["external_reference"] = payment_intent["id"]
data["source"] = "stripe"
return data
def create(self, validated_data):
try:
if validated_data['type'] == 'deposit':
return Payment.deposit(validated_data['owner'], validated_data['amount'], validated_data['source'], currency=validated_data['currency'], notes=validated_data['notes'])
else:
return Payment.objects.create(**validated_data)
except CardError as err:
raise serializers.ValidationError(err.user_message)
class BalanceSerializer(serializers.Serializer):
balance = serializers.DecimalField(max_digits=AMOUNT_MAX_DIGITS, decimal_places=AMOUNT_DECIMALS)

View File

@ -94,8 +94,11 @@ def get_card_from_payment(user, payment_method_id):
@handle_stripe_error
def attach_payment_method(payment_method_id, customer_id):
return stripe.PaymentMethod.attach(payment_method_id, customer=customer_id)
def attach_payment_method(payment_method_id, user):
customer_id = get_customer_id_for(user)
ret = stripe.PaymentMethod.attach(payment_method_id, customer=customer_id)
sync_cards_for_user(user)
return ret
@handle_stripe_error
def create_customer(name, email):
@ -107,7 +110,6 @@ def get_customer(customer_id):
@handle_stripe_error
def get_customer_cards(customer_id):
print(f"getting cards for: {customer_id}")
cards = []
stripe_cards = stripe.PaymentMethod.list(
@ -127,6 +129,10 @@ def get_customer_cards(customer_id):
return cards
@handle_stripe_error
def delete_card(card_id):
return stripe.PaymentMethod.detach(card_id)
def sync_cards_for_user(user):
customer_id = get_customer_id_for(user)
cards = get_customer_cards(customer_id)

9
uncloud_pay/urls.py Normal file
View File

@ -0,0 +1,9 @@
from django.urls import path, include
from django.conf import settings
from .views import *
app_name = 'uncloud_pay'
urlpatterns = [
path('cards/activate', CardActivateView.as_view(), name='card_activate'),
]

View File

@ -38,14 +38,32 @@ logger = logging.getLogger(__name__)
class PricingView(View):
def get(self, request, **args):
address = get_billing_address_for_user(self.request.user)
vat_rate = False
vat_validation_status = False
if address:
vat_rate = VATRate.get_vat_rate(address)
vat_validation_status = "verified" if address.vat_number_validated_on and address.vat_number_verified else False
pricing = get_order_total_with_vat(
request.GET.get('cores'),
request.GET.get('memory'),
request.GET.get('storage'),
pricing_name = args['name']
pricing_name = args['name'],
vat_rate = vat_rate * 100,
vat_validation_status = vat_validation_status
)
return JsonResponse(pricing)
class CardActivateView(View):
def post(self, request, **args):
card_id = request.POST.get('card_id')
if card_id:
matched_card = StripeCreditCard.objects.filter(owner=self.request.user, card_id=card_id).first()
matched_card.activate()
return JsonResponse({'success': 1})
else:
return JsonResponse({'error': "Please select a card"})
class RegisterCard(TemplateView):
template_name = "uncloud_pay/register_stripe.html"
@ -55,7 +73,6 @@ class RegisterCard(TemplateView):
def get_context_data(self, **kwargs):
customer_id = uncloud_stripe.get_customer_id_for(self.request.user)
setup_intent = uncloud_stripe.create_setup_intent(customer_id)
context = super().get_context_data(**kwargs)