From 5564400ef8819a61c82d5b2ab6b57c1e2fac5672 Mon Sep 17 00:00:00 2001 From: Amal Elshihaby Date: Fri, 30 Jul 2021 09:04:32 +0200 Subject: [PATCH] Refactor the Payment Model and Handle Order Confirmation Page --- .../static/matrixhosting/js/order.js | 4 +- .../static/matrixhosting/js/payment.js | 26 ++- .../templates/matrixhosting/index.html | 75 ++++--- .../matrixhosting/order_confirmation.html | 206 +++++++----------- .../matrixhosting/order_details.html | 20 +- matrixhosting/urls.py | 3 +- matrixhosting/views.py | 118 ++++------ uncloud/.env | 2 +- uncloud/urls.py | 2 +- .../migrations/0023_auto_20210730_1342.py | 23 ++ .../migrations/0024_auto_20210730_1441.py | 23 ++ uncloud_pay/models.py | 30 ++- uncloud_pay/selectors.py | 16 +- uncloud_pay/utils.py | 16 +- uncloud_pay/views.py | 15 +- 15 files changed, 312 insertions(+), 267 deletions(-) create mode 100644 uncloud_pay/migrations/0023_auto_20210730_1342.py create mode 100644 uncloud_pay/migrations/0024_auto_20210730_1441.py diff --git a/matrixhosting/static/matrixhosting/js/order.js b/matrixhosting/static/matrixhosting/js/order.js index af733bf..f910ba4 100644 --- a/matrixhosting/static/matrixhosting/js/order.js +++ b/matrixhosting/static/matrixhosting/js/order.js @@ -18,7 +18,7 @@ $( document ).ready(function() { modal_btn = $('#createvm-modal-done-btn'); if (data.error) { // Display error.message in your UI. - modal_btn.attr('href', error_url).removeClass('visually-hidden'); + modal_btn.attr('href', error_url).removeClass('sr-only sr-only-focusable'); fa_icon.attr('class', 'fa fa-close'); modal_btn.attr('class', '').addClass('btn btn-danger btn-ok btn-wide'); $('#createvm-modal-title').text("Error Occurred"); @@ -26,7 +26,7 @@ $( document ).ready(function() { } else { // The payment has succeeded // Display a success message - modal_btn.attr('href', data.redirect).removeClass('visually-hidden'); + modal_btn.attr('href', data.redirect).removeClass('sr-only sr-only-focusable'); $('#createvm-modal-title').text("Order Succeeded"); $('#createvm-modal-body').html("Order has been added and the instance will be ready soon"); } diff --git a/matrixhosting/static/matrixhosting/js/payment.js b/matrixhosting/static/matrixhosting/js/payment.js index f52aa25..dd4f70d 100644 --- a/matrixhosting/static/matrixhosting/js/payment.js +++ b/matrixhosting/static/matrixhosting/js/payment.js @@ -152,11 +152,29 @@ $(document).ready(function () { $(element).closest('.form-group').append(error); } }); + + $('#checkout-btn').click(function () { + if($('input[name="payment_card"]:checked').size() == 1) { + var id = $('input[name="payment_card"]:checked').val(); + if (id != 'new') { + $('#id_card').val(id); + submitBillingForm(id); + } + } + }); + $('#continue-btn').click(function () { + submitBillingForm(); + }); - $('.credit-card-info .btn.choice-btn').click(function () { - var id = this.dataset['id_card']; - $('#id_card').val(id); - submitBillingForm(id); + + $('input[name="payment_card"]').change(function(e) { + if($('input[name="payment_card"]:checked').val() == 'new') { + $('#checkout-btn').hide(); + $('#newcard').show(); + } else { + $('#newcard').hide(); + $('#checkout-btn').show(); + } }); }); diff --git a/matrixhosting/templates/matrixhosting/index.html b/matrixhosting/templates/matrixhosting/index.html index 8087a3d..7d33ecc 100644 --- a/matrixhosting/templates/matrixhosting/index.html +++ b/matrixhosting/templates/matrixhosting/index.html @@ -48,7 +48,7 @@

What you will get?

-

Lisque persius interesset his et, in quot quidam persequeris vim, ad mea essent possim iriure.

+

A secure chat that does not depend on a single point of failure

@@ -56,28 +56,42 @@
+ +
+
+
+
+
@@ -147,13 +161,12 @@

As simple as 1-2-3

-

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

@@ -171,7 +184,7 @@
- +
@@ -201,32 +214,32 @@ @@ -303,50 +316,56 @@
-
Lisque persius interesset his et, in quot quidam persequeris vim, ad mea essent possim iriure. Mutat tacimates id sit. Ridens mediocritatem ius an, eu nec magna imperdiet.
+
+

Yes! You will have to give us three domain names:

+

a) the homeserver: this is where the actual server is running - this can be on domain "A" - in case of ungleich we use ungleich.matrix.ungleich.cloud and give away YOURNAME.matrix.ungleich.cloud for free

+

b) the address of the web client - this is where people with their webbrowser go to - this should be different from "A". Often this is something like chat.example.orgor matrix.example.org. In case of ungleich this domain is matrix.ungleich.ch

+

c) the main matrix domain: the one you use for users and rooms. This is usually your main domain and is different from A. For ungleich this is ungleich.ch. Most people will choose their "main domain", for instance example.org here.

+
-
Iisque Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo.
+
No, since your homeserver will federate with the broader network
-
Iisque persius interesset his et, in quot quidam persequeris vim, ad mea essent possim iriure. Mutat tacimates id sit. Ridens mediocritatem ius an, eu nec magna imperdiet.
+
Video & Phone is handled by a jitsi server by default - matrix adds it as an integration, but does not handle video/audio directly. So the answer is: not E2EE for audio/video. +
-
Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo.
+
Once you change the initial password we do not have external access to the software anymore but we have access to the underlying server since we manage it: we can read and change things in the database 'by hand' since we have physical access to it. However end-to-end encrypted rooms stay secure. The content is encrypted with the user's keys and to us it will be shown in ciphertext.
-
Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo.
+
We do not enforce a limit of the number of users: you can do anythign you want as long as you fit the resources allocated to your homeserver. You are provided with 1GB of memory, 1vCPU and 20GB of storage with the base offer, which can be extended on demand (Pricing is the same as ipv6onlyhosting VMs, since that's what we use underneath).
-
Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo.
+
We recommend and provide you a web version of the Element client (desktop and mobile) but you can use any matrix client.
diff --git a/matrixhosting/templates/matrixhosting/order_confirmation.html b/matrixhosting/templates/matrixhosting/order_confirmation.html index 3a15284..a7b542c 100644 --- a/matrixhosting/templates/matrixhosting/order_confirmation.html +++ b/matrixhosting/templates/matrixhosting/order_confirmation.html @@ -40,29 +40,7 @@ {% endif %} {% if not error %} -
-
-
-
{% trans "Billed To" %}
-

- {% with request.session.billing_address_data as billing_address %} - {{billing_address.full_name}}
- {{billing_address.street}}, {{billing_address.postal_code}}
- {{billing_address.city}}, {{billing_address.country}} - {% if billing_address.vat_number %} -
{% trans "VAT Number" %} {{billing_address.vat_number}} - {% if pricing.vat_country != "ch" and pricing.vat_validation_status != "not_needed" %} - {% if pricing.vat_validation_status == "verified" %} - - {% else %} - - {% endif %} - {% endif %} - {% endif %} - {% endwith %} -

-
-
+
@@ -91,109 +69,92 @@ {% trans "Available Balance"%}
-
-
-
{% trans "Order summary" %}
-

- {% trans "Product" %}:  - Matrix Chat Hosting -

-
-
-

- {% trans "Cores" %}: - {{order.cores}} -

-

- {% trans "Memory" %}: - {{order.memory}} GB -

-

- {% trans "Disk space" %}: - {{order.storage}} GB -

-
-
-
-
-
-

- {% trans "Price Before VAT" %} - {{pricing.subtotal|floatformat:2}} CHF -

-
-
-
-
-
-
-
-

-
-
-

{% trans "Pre VAT" %}

-
-
-

{% trans "With VAT for" %} {{pricing.vat_country}} ({{pricing.vat_percent}}%)

-
-
-
-
-

Subtotal

-
-
-

{{pricing.subtotal|floatformat:2}} CHF

-
-
-

{{pricing.price_with_vat|floatformat:2}} CHF

-
-
- {% if pricing.discount.amount > 0 %} -
-
-

{{pricing.discount.name}}

-
-
-

-{{pricing.discount.amount|floatformat:2}} CHF

-
-
-

-{{pricing.discount.amount_with_vat|floatformat:2}} CHF

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

Total

-
-
-

{{pricing.subtotal_after_discount|floatformat:2}} CHF

-
-
-

{{pricing.price_after_discount_with_vat|floatformat:2}} CHF

-
-
-
-
-
-
-
- {% trans "Your Price in Total" %} - {{pricing.total_price|floatformat:2}} CHF -
-
+

+
Matrix Chat Hosting
+
+
+ + + + + + + + + + + + + +
{% trans "Cores" %}{% trans "Memory" %}{% trans "Disk space" %}
{{order.cores}}{{order.memory}} GB{{order.storage}} GB
+
+
+
+
+
+
+
+
+

Subtotal +

+
+

{{pricing.subtotal|floatformat:2}} CHF

+
+
+ {% if pricing.discount.amount > 0 %} +
+
+

{{pricing.discount.name }}

+
+
+

-{{pricing.discount.amount|floatformat:2}} CHF

+
+
+ {% endif %} +
-
+

+
+
+
+
+
+

{% trans "" %}

+
+
+

{{pricing.subtotal_after_discount|floatformat:2}} CHF

+
+
+
+
+

{% trans "VAT for" %} {{pricing.vat_country}} ({{pricing.vat_percent}}%)

+
+
+

CHF

+
+
+
+
+
+
+
+
+
+

{% trans "Total" %}

+
+
+

{{pricing.total|floatformat:2}} CHF

+
+
+
+
+
{% csrf_token %}
-
{% blocktrans with vm_total_price=vm.total_price|floatformat:2 %}By clicking "Place order" you agree to our Terms of Service and this plan will charge your credit card account with {{ vm_total_price }} CHF/month{% endblocktrans %}.
+ By clicking "Place order" you agree to our Terms of Service and this plan will charge your account balance with {{pricing.total|floatformat:2}} CHF

- {% with cards_len=cards|length %} + {% if show_cards %} +
+ {% with cards_len=cards|length %}

{% if cards_len > 0 %} {% blocktrans %}Please select one of the cards that you used before or fill in your credit card information below.{% endblocktrans %} {% else %} - {% blocktrans %}Please fill in your credit card information below.{% endblocktrans %} + {% blocktrans %}You haven't any active cards, Please fill in your credit card information below.{% endblocktrans %} {% endif %}

@@ -226,11 +228,14 @@
{% endfor %} {% if cards_len > 0 %} -
+
-
+
+ +
+
{% include "matrixhosting/includes/_card.html" %}
@@ -240,6 +245,12 @@ {% endif %}
{% endwith %} +
+ {% else %} +
+ +
+ {% endif %}
@@ -258,6 +269,7 @@ (function () { window.stripeKey = "{{stripe_key}}"; window.current_lan = "{{LANGUAGE_CODE}}"; + window.hasCreditcard = "{{show_cards}}"; })(); {%endif%} diff --git a/matrixhosting/urls.py b/matrixhosting/urls.py index f3015bd..367cc8d 100644 --- a/matrixhosting/urls.py +++ b/matrixhosting/urls.py @@ -2,12 +2,11 @@ from django.urls import path, include from django.conf import settings from django.conf.urls.static import static -from .views import IndexView, PricingView, OrderPaymentView, OrderDetailsView, Dashboard +from .views import IndexView, OrderPaymentView, OrderDetailsView, Dashboard app_name = 'matrixhosting' urlpatterns = [ - path('pricing//calculate/', PricingView.as_view(), name='pricing_calculator'), path('order/new/', OrderPaymentView.as_view(), name='payment'), path('order/confirm/', OrderDetailsView.as_view(), name='order_confirmation'), path('dashboard/', Dashboard.as_view(), name='dashboard'), diff --git a/matrixhosting/views.py b/matrixhosting/views.py index 9cb33e1..e99a7a0 100644 --- a/matrixhosting/views.py +++ b/matrixhosting/views.py @@ -1,5 +1,6 @@ import logging import json +import decimal from django.shortcuts import redirect, render from django.contrib import messages @@ -29,16 +30,6 @@ from .serializers import * logger = logging.getLogger(__name__) -class PricingView(View): - def get(self, request, **args): - subtotal, subtotal_after_discount, price_after_discount_with_vat, vat, vat_percent, discount = get_order_total_with_vat( - request.GET.get('cores'), - request.GET.get('memory'), - request.GET.get('storage'), - pricing_name = args['name'] - ) - return JsonResponse({'subtotal': subtotal, 'total': price_after_discount_with_vat}) - class IndexView(FormView): template_name = "matrixhosting/index.html" form_class = InitialRequestForm @@ -53,16 +44,14 @@ class IndexView(FormView): def form_valid(self, form): self.request.session['order'] = form.cleaned_data - subtotal, subtotal_with_discount, total, vat, vat_percent, discount = get_order_total_with_vat( + pricing = get_order_total_with_vat( form.cleaned_data['cores'], form.cleaned_data['memory'], form.cleaned_data['storage'], form.cleaned_data['pricing_name'], False ) - self.request.session['pricing'] = {'name': form.cleaned_data['pricing_name'], - 'subtotal': subtotal, 'vat': vat, 'total': total, - 'vat_percent': vat_percent, 'discount': discount} + self.request.session['pricing'] = pricing return HttpResponseRedirect(reverse('matrix:payment')) @@ -91,22 +80,24 @@ class OrderPaymentView(FormView): details_form = RequestHostedVMForm( initial=self.request.session.get('order', {}) ) + balance = get_balance_for_user(self.request.user) customer_id = uncloud_stripe.get_customer_id_for(self.request.user) cards = uncloud_stripe.get_customer_cards(customer_id) context.update({ 'matrix_vm_pricing': PricingPlan.get_by_name(self.request.session.get('pricing', {'name': 'unknown'})['name']), 'billing_address_form': billing_address_form, 'details_form': details_form, + 'balance': balance, 'cards': cards, - 'balance': get_balance_for_user(self.request.user), - 'stripe_key': settings.STRIPE_PUBLIC_KEY + 'stripe_key': settings.STRIPE_PUBLIC_KEY, + 'show_cards': True if balance < self.request.session.get('pricing')['total'] else False, }) return context @cache_control(no_cache=True, must_revalidate=True, no_store=True) def get(self, request, *args, **kwargs): - for k in ['vat_validation_status', 'token', 'id_payment_method']: + for k in ['vat_validation_status', 'token', 'id_payment_method', 'total']: if request.session.get(k): request.session.pop(k) if 'order' not in request.session: @@ -121,14 +112,11 @@ class OrderPaymentView(FormView): context.update({'details_form': details_form, 'billing_address_form': billing_address_form}) return self.render_to_response(context) - id_payment_method = self.request.POST.get('id_payment_method', None) - self.request.session["id_payment_method"] = id_payment_method + this_user = { 'email': self.request.user.email, 'username': self.request.user.username } - customer_id = uncloud_stripe.get_customer_id_for(self.request.user) - uncloud_stripe.attach_payment_method(id_payment_method, customer_id) address = get_billing_address_for_user(self.request.user) if address: form = BillingAddressForm(self.request.POST, instance=address) @@ -139,15 +127,16 @@ class OrderPaymentView(FormView): self.request.session["billing_address_id"] = billing_address_ins.id self.request.session['billing_address_data'] = billing_address_form.cleaned_data self.request.session['billing_address_data']['owner'] = self.request.user.id - self.request.session['user'] = this_user - self.request.session['customer'] = customer_id + id_payment_method = self.request.POST.get('id_payment_method', False) + customer_id = uncloud_stripe.get_customer_id_for(self.request.user) + if id_payment_method and id_payment_method != 'undefined': + uncloud_stripe.attach_payment_method(id_payment_method, customer_id) vat_number = billing_address_form.cleaned_data.get('vat_number').strip() if vat_number: validate_result = validate_vat_number( stripe_customer_id=customer_id, billing_address_id=billing_address_ins.id ) - if 'error' in validate_result and validate_result['error']: messages.add_message( self.request, messages.ERROR, validate_result["error"], @@ -157,6 +146,21 @@ class OrderPaymentView(FormView): reverse('matrix:payment') + '#vat_error' ) self.request.session["vat_validation_status"] = validate_result["status"] + specs = details_form.cleaned_data + vat_rate = VATRate.get_vat_rate(billing_address_ins) + vat_validation_status = "verified" if billing_address_ins.vat_number_validated_on and billing_address_ins.vat_number_verified else False + pricing = get_order_total_with_vat( + 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): + payment_id = Payment.deposit(request.user, abs(amount), source='stripe') + print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>") + print(payment_id) + self.request.session['pricing'] = pricing + self.request.session['order'] = specs + self.request.session['vat_validation_status'] = vat_validation_status return HttpResponseRedirect(reverse('matrix:order_confirmation')) class OrderDetailsView(DetailView): @@ -164,69 +168,35 @@ class OrderDetailsView(DetailView): context_object_name = "order" model = Order - # @method_decorator(login_required) - # def dispatch(self, *args, **kwargs): - # return super().dispatch(*args, **kwargs) + @method_decorator(login_required) + def dispatch(self, *args, **kwargs): + return super().dispatch(*args, **kwargs) @cache_control(no_cache=True, must_revalidate=True, no_store=True) def get(self, request, *args, **kwargs): - context = {} - # if ('order' not in request.session or 'user' not in request.session): - # return HttpResponseRedirect(reverse('matrix:index')) - # if 'id_payment_method' in self.request.session: - # card = uncloud_stripe.get_card_from_payment(self.request.user, self.request.session['id_payment_method']) - # if not card: - # return HttpResponseRedirect(reverse('matrix:payment')) - # context['card'] = card - # elif 'id_payment_method' not in self.request.session or 'vat_validation_status' not in self.request.session: - # return HttpResponseRedirect(reverse('matrix:payment')) - # specs = request.session.get('order') - # pricing = request.session.get('pricing') - # billing_address = BillingAddress.objects.get(id=request.session.get('billing_address_id')) - # vat_rate = VATRate.get_vat_rate(billing_address) - # vat_validation_status = "verified" if billing_address.vat_number_validated_on and billing_address.vat_number_verified else False - # subtotal, subtotal_after_discount, price_after_discount_with_vat, vat, vat_percent, discount = get_order_total_with_vat( - # specs['cores'], specs['memory'], specs['storage'], request.session['pricing']['name'], - # vat_rate=vat_rate * 100, vat_validation_status = vat_validation_status - # ) - # pricing = { - # "subtotal": subtotal, "discount": discount, "vat": vat, "vat_percent": vat_percent, - # "vat_country": billing_address.country.lower(), - # "subtotal_after_discount": subtotal_after_discount, - # "price_after_discount_with_vat": price_after_discount_with_vat - - # } - # pricing["price_with_vat"] = round(subtotal * (1 + pricing["vat_percent"] * 0.01), 2) - # discount["amount_with_vat"] = round(pricing["price_with_vat"] - pricing["price_after_discount_with_vat"], 2) - # pricing["total_price"] = pricing["price_after_discount_with_vat"] - # self.request.session['total_price'] = pricing["price_after_discount_with_vat"] - # payment_intent_response = uncloud_stripe.get_payment_intent(request.user, pricing["price_after_discount_with_vat"]) - # context.update({ - # 'payment_intent_secret': payment_intent_response.client_secret, - # 'order': specs, - # 'pricing': pricing, - # 'stripe_key': settings.STRIPE_PUBLIC_KEY, - # }) + context = { + 'order': self.request.session.get('order'), + 'pricing': self.request.session.get('pricing'), + 'balance': get_balance_for_user(self.request.user) + } + 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 render(request, self.template_name, context) def post(self, request, *args, **kwargs): customer = StripeCustomer.objects.get(owner=self.request.user) - billing_address = BillingAddress.objects.get(id=request.session.get('billing_address_id')) - if 'id_payment_method' in request.session: - card = uncloud_stripe.get_card_from_payment(self.request.user, self.request.session['id_payment_method']) - if not card: - return show_error("There was a payment related error.", self.request) - else: - return show_error("There was a payment related error.", self.request) - + billing_address = BillingAddress.objects.get(id=request.session.get('billing_address_id')) + total = self.request.session['pricing']['total'] order = finalize_order(request, customer, billing_address, - self.request.session['total_price'], + total, PricingPlan.get_by_name(self.request.session['pricing']['name']), request.session.get('order')) if order: bill = Bill.create_next_bill_for_user_address(billing_address) - payment= Payment.objects.create(owner=request.user, amount=self.request.session['total_price'], source='stripe') + 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() diff --git a/uncloud/.env b/uncloud/.env index b136ca2..6d84ebb 100644 --- a/uncloud/.env +++ b/uncloud/.env @@ -1,7 +1,7 @@ ALLOWED_HOSTS= STRIPE_KEY= STRIPE_PUBLIC_KEY= -DATABASE_ENGINE= +DATABASE_ENGINE=django.db.backends.sqlite3 DATABASE_NAME= DATABASE_HOST= DATABASE_PORT= diff --git a/uncloud/urls.py b/uncloud/urls.py index 179b7c3..084bdac 100644 --- a/uncloud/urls.py +++ b/uncloud/urls.py @@ -58,8 +58,8 @@ urlpatterns = [ ), name='openapi-schema'), path('admin/', admin.site.urls), - path('accounts/', include('allauth.urls')), + path('pricing//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('', include('matrixhosting.urls', namespace='matrix')), diff --git a/uncloud_pay/migrations/0023_auto_20210730_1342.py b/uncloud_pay/migrations/0023_auto_20210730_1342.py new file mode 100644 index 0000000..e2e4c6f --- /dev/null +++ b/uncloud_pay/migrations/0023_auto_20210730_1342.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.4 on 2021-07-30 13:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0022_remove_order_status'), + ] + + operations = [ + migrations.AddField( + model_name='payment', + name='notes', + field=models.TextField(blank=True, default='', null=True), + ), + migrations.AddField( + model_name='payment', + name='type', + field=models.CharField(choices=[('send', 'Send Money'), ('receive', 'Receive Money')], default='send', max_length=256), + ), + ] diff --git a/uncloud_pay/migrations/0024_auto_20210730_1441.py b/uncloud_pay/migrations/0024_auto_20210730_1441.py new file mode 100644 index 0000000..97baebc --- /dev/null +++ b/uncloud_pay/migrations/0024_auto_20210730_1441.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.4 on 2021-07-30 14:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0023_auto_20210730_1342'), + ] + + operations = [ + migrations.AlterField( + model_name='payment', + name='source', + field=models.CharField(blank=True, choices=[('wire', 'Wire Transfer'), ('stripe', 'Stripe'), ('voucher', 'Voucher'), ('referral', 'Referral')], max_length=256, null=True), + ), + migrations.AlterField( + model_name='payment', + name='type', + field=models.CharField(choices=[('withdraw', 'Withdraw Money'), ('deposit', 'Deposit Money')], default='send', max_length=256), + ), + ] diff --git a/uncloud_pay/models.py b/uncloud_pay/models.py index a4cf007..3d0461f 100644 --- a/uncloud_pay/models.py +++ b/uncloud_pay/models.py @@ -78,6 +78,13 @@ class StripeCreditCard(models.Model): class Payment(models.Model): owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) + type = models.CharField(max_length=256, + choices = ( + ('withdraw', 'Withdraw Money'), + ('deposit', 'Deposit Money') + ), null=False, blank=False, default="send") + + notes = models.TextField(default="", null=True, blank=True) amount = models.DecimalField( max_digits=AMOUNT_MAX_DIGITS, @@ -90,7 +97,7 @@ class Payment(models.Model): ('stripe', 'Stripe'), ('voucher', 'Voucher'), ('referral', 'Referral'), - )) + ), null=True, blank=True,) timestamp = models.DateTimeField(default=timezone.now) @@ -101,19 +108,24 @@ class Payment(models.Model): def __str__(self): return f"{self.amount}{self.currency} from {self.owner} via {self.source} on {self.timestamp}" - def save(self, *args, **kwargs): - # Try to charge the user via the active card before saving otherwise throw payment Error - if self.source == 'stripe': + @classmethod + def deposit(cls, owner, amount, source, currency='CHF', notes=''): + if source == 'stripe': try: - result = uncloud_pay.stripe.charge_customer(self.owner, self.amount, self.currency,) - if not result.status or result.status != 'succeeded': + payment_intent = uncloud_pay.stripe.charge_customer(owner, amount, currency) + if not payment_intent.status or payment_intent.status != 'succeeded': raise Exception("The payment has been failed, please try to activate another card") - super().save(*args, **kwargs) + return cls.objects.create(owner=owner, type="deposit", amount=amount, external_reference=payment_intent["id"], + currency=currency, source=source, notes=notes) except Exception as e: raise e - - + @classmethod + def withdraw(cls, owner, amount, currency='CHF', notes=''): + 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): """ diff --git a/uncloud_pay/selectors.py b/uncloud_pay/selectors.py index a634e30..c1eb64a 100644 --- a/uncloud_pay/selectors.py +++ b/uncloud_pay/selectors.py @@ -2,23 +2,17 @@ from django.utils import timezone from django.db import transaction from .models import * -def get_payments_for_user(user): - payments = [ payment.amount for payment in Payment.objects.filter(owner=user) ] - +def get_deposit_payments_for_user(user): + payments = [ payment.amount for payment in Payment.objects.filter(owner=user, type='deposit')] return sum(payments) def get_spendings_for_user(user): - bills = Bill.objects.filter(owner=user) - - amount = 0 - for bill in bills: - amount += bill.sum - - return amount + spendings = [payment.amount for payment in Payment.objects.filter(owner=user, type='withdraw')] + return sum(spendings) @transaction.atomic def get_balance_for_user(user): - return get_payments_for_user(user) - get_spendings_for_user(user) + return get_deposit_payments_for_user(user) - get_spendings_for_user(user) @transaction.atomic def has_enough_balance(user, due_amount): diff --git a/uncloud_pay/utils.py b/uncloud_pay/utils.py index c80ae3a..cf5d09c 100644 --- a/uncloud_pay/utils.py +++ b/uncloud_pay/utils.py @@ -103,7 +103,7 @@ def create_tax_id(stripe_customer_id, billing_address_id, type): } def apply_vat_discount(subtotal, pricing_plan, vat_rate=False, vat_validation_status=False): - vat_percent = vat_rate or pricing_plan.vat_percentage + vat_percent = vat_rate if pricing_plan.vat_inclusive or (vat_validation_status and vat_validation_status in ["verified", "not_needed"]): vat_percent = decimal.Decimal(0) vat = decimal.Decimal(0) @@ -119,7 +119,7 @@ def apply_vat_discount(subtotal, pricing_plan, vat_rate=False, vat_validation_st subtotal = round(float(subtotal), 2) vat_percent = round(float(vat_percent), 2) discount = { - 'name': pricing_plan.discount_name, + 'name': pricing_plan.discount_name or 'Discount', 'amount': discount_amount, 'amount_with_vat': round(float(discount_amount_with_vat), 2) } @@ -149,7 +149,17 @@ def get_order_total_with_vat(cores, memory, storage, (decimal.Decimal(memory) * pricing.ram_unit_price) + (decimal.Decimal(storage) * (pricing.storage_unit_price)) ) - return apply_vat_discount(subtotal, pricing, vat_rate, vat_validation_status) + subtotal, subtotal_after_discount, price_after_discount_with_vat, vat, vat_percent, discount = \ + apply_vat_discount(subtotal, pricing, vat_rate, vat_validation_status) + return { + "name": pricing.name, + "subtotal": subtotal, + "discount": discount, + "vat": vat, "vat_percent": vat_percent, + "vat_validation_status": vat_validation_status, + "subtotal_after_discount": subtotal_after_discount, + "total": price_after_discount_with_vat + } diff --git a/uncloud_pay/views.py b/uncloud_pay/views.py index e90dda1..3bd3c9e 100644 --- a/uncloud_pay/views.py +++ b/uncloud_pay/views.py @@ -1,5 +1,5 @@ from django.contrib.auth.mixins import LoginRequiredMixin -from django.views.generic.base import TemplateView +from django.views.generic.base import TemplateView, View from django.shortcuts import render from django.db import transaction from django.contrib.auth import get_user_model @@ -23,6 +23,7 @@ import logging from .models import * from .serializers import * from .selectors import * +from .utils import get_order_total_with_vat from datetime import datetime from vat_validator import sanitize_vat @@ -34,8 +35,16 @@ import stripe logger = logging.getLogger(__name__) -### -# 2020-12 checked code + +class PricingView(View): + def get(self, request, **args): + pricing = get_order_total_with_vat( + request.GET.get('cores'), + request.GET.get('memory'), + request.GET.get('storage'), + pricing_name = args['name'] + ) + return JsonResponse(pricing) class RegisterCard(TemplateView): template_name = "uncloud_pay/register_stripe.html"