Compare commits

...

424 commits

Author SHA1 Message Date
Tomislav R
338ff38bbb Actual po 2022-05-16 11:37:30 +02:00
Tomislav R
66f3989c23 WIP removing ipv6onlyhosting.{net,ch} references 2022-05-16 11:08:14 +02:00
app@dynamicweb-production
a21b4d6e3f Change to dcl-orders for new vm 2022-03-23 10:46:12 +01:00
d739a4a50e Merge pull request 'Change admin email to dcl-orders for vm purchase' (#12) from change-email-address into master
Reviewed-on: ungleich-public/dynamicweb#12
2022-02-05 03:19:01 +00:00
PCoder
138fd519b7 Change admin email to dcl-orders for vm purchase 2022-02-05 08:30:58 +05:30
Nico Schottelius
8179ca4d22 Add support for docker build + docker release 2021-12-17 22:21:59 +01:00
Nico Schottelius
c0333212aa Begin updating for dockerisation 2021-12-17 22:02:20 +01:00
4861bee9d3 Merge branch 'master' into 'master'
Fix poland country code in eu_countries

See merge request ungleich-public/dynamicweb!749
2021-09-30 10:32:26 +02:00
5ce283318a Fix poland country code in eu_countries 2021-09-27 09:21:24 +02:00
79e96715b2 Merge branch 'issue/1/user-password-update' into 'master'
Issue/1/user password update

See merge request ungleich-public/dynamicweb!748
2021-08-30 15:20:27 +02:00
PCoder
47d5c63e3b Fix bad import 2021-08-30 18:38:58 +05:30
PCoder
d26f2b0f69 Normalize/convert ascii/ignore unicode characters for homeDirectory 2021-08-30 18:29:42 +05:30
7c2c3de1f6 Merge branch '9053/show-paid-invoices-only' into 'master'
Filter invoices by paid status

See merge request ungleich-public/dynamicweb!747
2021-03-29 03:59:48 +02:00
PCoder
1d48dfb93b Filter invoices by paid status 2021-03-29 07:23:58 +05:30
PCoder
63821813d4 Fix translations 2021-02-07 18:05:48 +05:30
PCoder
173c0fe9bf Update texts 2021-02-07 17:28:44 +05:30
PCoder
6279fa4e7b Fix missing escapes 2021-02-07 16:21:02 +05:30
PCoder
1fec2add72 Update Changelog for 3.2 2021-02-07 16:07:23 +05:30
PCoder
d46deaa23a Do not use UserCardDetail to save card 2021-02-07 15:58:03 +05:30
PCoder
a5c83dd589 Update order confirmation text to better prepared for payment dispute 2021-02-07 15:55:47 +05:30
PCoder
af09b343c0 Update Changelog for 3.1 2021-01-12 13:56:58 +05:30
PCoder
3b874901bc Update using correct card details 2021-01-12 13:40:03 +05:30
PCoder
640807eb62 Don't get card from local db 2021-01-12 13:39:11 +05:30
PCoder
21f762d6b8 Update Changelog for 3.0 2021-01-07 16:39:00 +05:30
9e640d0802 Merge branch '8691/check_vm_templates' into 'master'
8691/check vm templates

See merge request ungleich-public/dynamicweb!744
2021-01-07 12:06:02 +01:00
PCoder
c58302d90e Log error messages 2021-01-07 16:30:48 +05:30
PCoder
1e67bef4f5 Remove unwanted code/comments 2021-01-07 16:30:33 +05:30
PCoder
ec13a71866 Reformat code 2021-01-07 16:29:34 +05:30
PCoder
6c968fdbb8 Redirect to dcl payment
We no longer seem to use hosting payment
2021-01-02 09:03:02 +05:30
PCoder
44ebb71916 Also clear id_payment_method from session 2021-01-02 09:02:37 +05:30
PCoder
1d7c3f424c Don't send admin email if IncompletePaymentIntent lookup doesn't contain a value 2021-01-01 11:17:07 +05:30
PCoder
8deed169ca Fix redirect url
Take user to virtual_machines page if the user ordered a VM
2021-01-01 11:13:02 +05:30
PCoder
7cd485bc6d Fix issues on settings/card save/delete methods 2021-01-01 02:37:52 +05:30
PCoder
31c5336e18 Show cards directly from Stripe and dissociate using PaymentMethod 2021-01-01 01:59:41 +05:30
PCoder
36505db5a2 set default_payment_method 2021-01-01 01:35:59 +05:30
PCoder
e7462289f6 Just log IncompleteSubscription does not exist error
Do not compare with db and send admin error message
2021-01-01 01:03:08 +05:30
PCoder
9b84461a29 Add logger messages 2021-01-01 00:58:11 +05:30
PCoder
04003757dc Move log block to correct case 2021-01-01 00:43:28 +05:30
PCoder
e024a3a7a6 Do not create paymentintent for subscription 2021-01-01 00:34:35 +05:30
PCoder
ba3c5ddd1d Use different js code for one-time and subscriptions 2021-01-01 00:09:47 +05:30
PCoder
2c3d00f03f Pass correct stripe_customer_id 2021-01-01 00:09:20 +05:30
PCoder
d8a674da3d Remove unwanted code + doc 2021-01-01 00:08:58 +05:30
PCoder
9524e03762 Pass user param with request dict 2021-01-01 00:08:33 +05:30
PCoder
a32a5af5a3 Add code to differentiate between subscription and non-subscription in js 2021-01-01 00:07:23 +05:30
PCoder
9faf897818 Formatting and documentation 2020-12-31 22:46:15 +05:30
PCoder
6e6a57b304 Refactor price to charge => amount_to_charge
This is a common variable between the generic onetime and
subscription
2020-12-31 22:45:05 +05:30
PCoder
7b71ba55f2 Rename token to id_payment_method 2020-12-31 22:43:50 +05:30
PCoder
8c72b56f6c Use setup_future_usage='off_session' 2020-12-31 21:03:24 +05:30
PCoder
d2ebd3c473 Do card association 2020-12-31 20:53:02 +05:30
PCoder
b36afcb828 Simplify code for logged in one-time payments with SCA 2020-12-31 20:35:31 +05:30
PCoder
a823efd8e2 Add get_available_payment_methods 2020-12-31 20:01:52 +05:30
PCoder
13f5f576b5 Fix getting cc details from payment_methods 2020-12-31 17:23:43 +05:30
PCoder
2f98294eab Store dicts as json in db 2020-12-31 17:12:17 +05:30
PCoder
213b9a068e Fix attribute name 2020-12-31 17:07:51 +05:30
PCoder
33f741424d See inner values of incomplete_pm 2020-12-31 17:05:01 +05:30
PCoder
7309b8416c Handle any exception 2020-12-31 17:02:03 +05:30
PCoder
ff7b20b0dc Stripe id is not a dict 2020-12-31 16:59:22 +05:30
PCoder
37f82a48d5 More loggers 2020-12-31 16:55:05 +05:30
PCoder
8827bd15ba More loggers 2020-12-31 16:47:45 +05:30
PCoder
8e4b3ce96b Store charge id from payement intent result 2020-12-31 16:39:25 +05:30
PCoder
85757e01c9 Save charge id 2020-12-31 16:37:25 +05:30
PCoder
f48a5cfe71 Set completed_at value 2020-12-31 16:29:18 +05:30
PCoder
52d1fb6a0e Add logger + return 200 on success of webhook 2020-12-31 16:27:12 +05:30
PCoder
12c9140b3a Fix using correct payment intent id 2020-12-31 16:23:44 +05:30
PCoder
7db0594778 Update IncompletePaymentIntents to allow null subscription id and charge id 2020-12-31 16:12:22 +05:30
PCoder
87b85c43b4 More logger 2020-12-31 15:59:09 +05:30
PCoder
b2f0a45679 Add logger message 2020-12-31 15:56:08 +05:30
PCoder
9ae4b96968 Add hosting/migrations/0064_incompletepaymentintents.py 2020-12-31 15:43:46 +05:30
PCoder
9077eb0cf2 Implement webhook 2020-12-31 15:41:43 +05:30
PCoder
98b5d03d0b Refactor code for do_provisioning_generic 2020-12-31 15:40:46 +05:30
PCoder
f628046417 Add IncompletePaymentIntents model 2020-12-31 15:25:14 +05:30
PCoder
41de724904 Handle PaymentMethod type in set_default_card 2020-12-31 15:24:47 +05:30
PCoder
ba92c8e416 Do not pop billing address data from session in case of a payment failure
Instead pop id_payment_method
2020-12-31 11:05:32 +05:30
PCoder
42c9ec6f28 Handle js success/error messages 2020-12-31 10:32:25 +05:30
PCoder
c3286a68a5 Use payment method instead of token and PaymentIntent all over 2020-12-31 10:04:21 +05:30
PCoder
35cc9d4229 Log client secret 2020-12-24 20:07:31 +05:30
PCoder
e9c596de66 Test PaymentIntent for payment of generic onetime products 2020-12-24 20:03:46 +05:30
PCoder
ec1da8fbdf Use cents price for Stripe 2020-12-24 19:40:39 +05:30
PCoder
1c4f297775 Begin migrating to PaymentIntent 2020-12-24 19:34:06 +05:30
PCoder
acba77976d Add missing formatting identifier 2020-12-24 06:55:19 +05:30
PCoder
624cc45c12 Log error dict 2020-12-24 06:51:34 +05:30
PCoder
82359064cd Handle creation of correctly 2020-12-24 06:47:33 +05:30
PCoder
98628596f0 Add params to docstring 2020-12-24 06:25:37 +05:30
PCoder
39c8e35eca Add logger messages 2020-12-24 06:25:22 +05:30
PCoder
eefabe45b6 Login new users only for non-SCA generic subscription payments only 2020-12-23 20:41:31 +05:30
PCoder
968eaaf6a4 For generic payments, take users to invoice page after purchase 2020-12-23 20:40:20 +05:30
PCoder
6a7373523e Set default card before making payments 2020-12-23 20:39:41 +05:30
PCoder
080a45f39c Pop up stale card_id/token or billing_address_data in the paymentorder page 2020-12-23 20:38:42 +05:30
PCoder
c8519058c4 Fix new_user_hosting_key_id 2020-12-23 18:31:45 +05:30
PCoder
a03e2dc006 Return provisioning if set in do_provisioning 2020-12-23 18:10:17 +05:30
PCoder
c28bd9091a Add more logger 2020-12-23 17:53:19 +05:30
PCoder
c0aeac4dc7 Add some logger messages 2020-12-23 17:47:50 +05:30
PCoder
377d12b5a5 Create IncompleteSubscriptions only for SCA case 2020-12-23 17:47:37 +05:30
PCoder
d447f8d9e6 Add logging message 2020-12-23 17:26:40 +05:30
PCoder
799194152e Remove todo 2020-12-23 17:16:19 +05:30
PCoder
78b8191165 Implement invoice.payment_failed case 2020-12-23 17:10:23 +05:30
PCoder
a9778076d6 Clean session variables using real_request 2020-12-23 17:10:06 +05:30
PCoder
a99924b94c Rename do_create_vm to do_provisioning; and pass real_request 2020-12-23 17:09:27 +05:30
PCoder
41e993a3d9 Log variable value 2020-12-23 16:34:34 +05:30
PCoder
0c1b7b1885 Do clear session vars at the end 2020-12-23 14:33:32 +05:30
PCoder
480e38fbc9 Attempt fix for local variable 'card_details_dict' referenced before assignment 2020-12-23 14:19:52 +05:30
PCoder
4962b72d1a Add logging message 2020-12-23 14:19:22 +05:30
PCoder
70c8ed6825 Add debugging messages 2020-12-23 14:12:30 +05:30
PCoder
259c509113 Don't handle generic exception for the moment 2020-12-23 14:02:27 +05:30
PCoder
981e68aa4f Fix getting card_id and compare 2020-12-23 13:56:08 +05:30
PCoder
a4a5acd0e7 Fix string formatting issues 2020-12-23 13:46:34 +05:30
PCoder
812157b6c6 Move login code out of the refactored do_create_vm 2020-12-23 13:40:19 +05:30
PCoder
9d765fcb6e Fix wrong variable name billing_address 2020-12-23 13:10:29 +05:30
PCoder
f6f6482ce0 Add missing billing_address_data 2020-12-23 13:07:28 +05:30
PCoder
2baa77a7d4 Fix json loads issue 2020-12-23 13:00:05 +05:30
PCoder
de6bc06eaf Fix json loads for none 2020-12-23 12:50:21 +05:30
PCoder
a5f49cf8be Add debug messages 2020-12-23 12:41:46 +05:30
PCoder
c70753767f Load request object correctly and pass correct subscription object 2020-12-23 12:32:42 +05:30
PCoder
92bafed3b3 Fix getting stripe_subscription_obj 2020-12-23 12:09:19 +05:30
PCoder
b1dd9988ce Add missing subscription_id param 2020-12-23 12:03:17 +05:30
PCoder
95a1b8fa20 Make complete_at allow null 2020-12-23 11:52:41 +05:30
PCoder
50d9eb1c50 Fix UnboundLocalError: local variable 'stripe_onetime_charge' referenced before assignment 2020-12-23 11:26:52 +05:30
PCoder
1ed42e608c Add incompletesubscriptions migration file 2020-12-23 11:07:20 +05:30
PCoder
17c8f9ca18 Handle IncompleteSubscriptions in webhook 2020-12-23 10:59:21 +05:30
PCoder
c4c918d591 Prepare params from session to pass to do_create_vm 2020-12-23 09:26:01 +05:30
PCoder
20c6703236 Add a todo 2020-12-23 09:10:03 +05:30
PCoder
2a84d20f35 Add docstring 2020-12-23 09:09:53 +05:30
PCoder
9f49c664fa Make do_create_vm independent of session 2020-12-23 09:09:40 +05:30
PCoder
9e247cc556 Fix PEP warning 2020-12-23 09:08:08 +05:30
PCoder
ca7481cce0 Avoid request.user.is_authenticated() 2020-12-23 08:00:08 +05:30
PCoder
3e95a389bb Preparing a fix for TODO (wip) 2020-12-23 07:58:20 +05:30
PCoder
cda241893b Fix PEP warning 2020-12-23 07:57:54 +05:30
PCoder
cb7a1ed4f4 Implement provisioning of VM on invoice.paid webhook 2020-12-20 02:41:30 +05:30
PCoder
3389e69af1 WIP: Begin handling of invoice.paid webhook 2020-12-18 17:34:40 +05:30
PCoder
a63fac1a20 Set data at the client side according to success or error 2020-12-18 17:16:44 +05:30
PCoder
d0d5fb0196 Handle payment_intent requires SCA case 2020-12-18 16:47:45 +05:30
PCoder
0b0c932e5a Refactor code: show_error 2020-12-18 16:45:16 +05:30
PCoder
7fcf148cf4 Merge remote-tracking branch 'mainRepo/master' into 8393/show-SCA 2020-12-18 14:11:46 +05:30
PCoder
504681107a Remove unreachable code 2020-12-10 10:04:16 +05:30
PCoder
57b6b18243 Fix PEP warning 2020-12-10 10:04:04 +05:30
PCoder
22d3b1f83c Add vm_type to the log info 2020-12-10 09:05:04 +05:30
PCoder
01d8cc1b9b Use absolute paths 2020-12-10 08:56:34 +05:30
PCoder
785091e4ff Handle vm_id is None case 2020-12-10 08:40:40 +05:30
PCoder
890a83cfa6 Add check_vm_templates management command 2020-12-10 08:34:17 +05:30
PCoder
585e9cf146 Merge branch 'feature/fix-vm-after-celery-error' 2020-12-07 11:39:06 +05:30
PCoder
a0ab436d9a Update Changelog for 2.14 2020-12-07 11:36:34 +05:30
abfbc3b69a Merge branch 'feature/fix-vm-after-celery-error' into 'master'
Feature/fix vm after celery error

See merge request ungleich-public/dynamicweb!743
2020-12-07 06:31:31 +01:00
PCoder
7bcca15f0b Cleanup logging 2020-12-07 10:52:06 +05:30
PCoder
9d85c058da Fetch correct cred 2020-12-07 10:48:48 +05:30
PCoder
8c374af4ff More logging 2020-12-07 10:45:51 +05:30
PCoder
ff28c6e8e8 Add log message 2020-12-07 10:44:20 +05:30
PCoder
bbb51b71a6 Fix variable name 2020-12-07 10:40:55 +05:30
PCoder
591f5ff37b Fix key 2020-12-07 10:37:12 +05:30
PCoder
81ba834b01 Add missing arguments 2020-12-07 10:31:45 +05:30
PCoder
082c0b00af Implement fix_vm_after_celery_error 2020-12-07 09:53:14 +05:30
PCoder
17557fd4c9 Create refactored method handle_metadata_and_emails 2020-12-07 09:52:53 +05:30
PCoder
352c780287 Work in progress 2020-12-07 07:53:20 +05:30
PCoder
e9801eb9c4 Fix wrong logging 2020-12-03 09:48:08 +05:30
PCoder
e522ac0f61 Merge branch 'master' into 8393/show-SCA 2020-12-02 18:49:31 +05:30
PCoder
bf1aad82b8 Update Changelog for 2.13 2020-12-02 18:38:37 +05:30
555e13e631 Merge branch 'bugfix/8654/500-error-on-invoices' into 'master'
Bugfix/8654/500 error on invoices

See merge request ungleich-public/dynamicweb!742
2020-12-01 12:48:05 +01:00
PCoder
d980fb0000 Quote email in links 2020-12-01 17:13:09 +05:30
PCoder
e8b79d6951 Return emtpty string when plan is not set 2020-12-01 17:12:55 +05:30
PCoder
52362cd0ea In case of error, log it and return empty result 2020-12-01 17:12:29 +05:30
73cb003353 Merge branch '8593/escape-ssh-key-str' into 'master'
Escape ssh key before storing

See merge request ungleich-public/dynamicweb!741
2020-11-12 08:02:11 +01:00
PCoder
79cbfac092 Escape ssh key before storing 2020-11-12 12:12:46 +05:30
PCoder
2973ef3b1d Use correct variable 2020-10-11 17:24:33 +05:30
PCoder
81ec1125cb Capture both actions requires_action and requires_source_action 2020-10-11 17:21:15 +05:30
PCoder
4c7b9eaa52 Handle SCA in dcl flow 2020-10-11 17:11:20 +05:30
PCoder
676a358832 Add logger messages 2020-10-11 16:57:10 +05:30
PCoder
877553e442 Add logger message 2020-10-11 16:45:23 +05:30
PCoder
70bfef4738 Show SCA modal when required 2020-10-11 16:01:55 +05:30
PCoder
c2e2e1828f Update Changelog for 2.12.1 2020-07-21 23:08:32 +05:30
PCoder
08bf163d21 Remove multiple hl 2020-07-21 22:26:16 +05:30
PCoder
df301a18fc Set vat params 2020-07-21 22:23:07 +05:30
PCoder
ad5371a133 Update order confirmation template 2020-07-21 22:15:04 +05:30
PCoder
fb59ae4055 Add migration 2020-07-21 22:03:18 +05:30
PCoder
050309a68e Add exclude_vat_calculations field and add VAT only when this is set to False 2020-07-21 21:54:54 +05:30
18c494dfd7 Merge branch '8198/case-1/update-DE-VATRate' into 'master'
Create migration for DE VAT update for COVID-19

See merge request ungleich-public/dynamicweb!739
2020-07-01 01:13:55 +02:00
PCoder
c339c19cfd Fix date format bug 2020-07-01 01:21:57 +05:30
PCoder
58377319b9 Make migration reversible 2020-07-01 01:18:57 +05:30
PCoder
87a154bd0a Create migration 2020-07-01 00:47:59 +05:30
PCoder
49a9fdd842 Update Changelog for 2.12 2020-06-23 11:32:30 +05:30
PCoder
0c39336653 Sort invoices by created_at desc 2020-06-22 20:04:01 +05:30
PCoder
81fd129d48 Use css to hide div than js 2020-06-19 16:05:06 +05:30
PCoder
25255c862a Remove buggy # in dom element id 2020-06-19 15:50:24 +05:30
PCoder
1b29a23ede Remove default display none of one-time-payments div 2020-06-19 15:48:00 +05:30
PCoder
6b3ecfaff4 Use show/hide instead of toggle 2020-06-19 15:45:44 +05:30
PCoder
c4e7f99202 Add slideToggles to subscription/one-time-payments divs 2020-06-19 15:38:26 +05:30
PCoder
0dc9c6cdca Fix element id 2020-06-19 15:30:38 +05:30
PCoder
38109e175a Move js to correct place 2020-06-19 15:25:34 +05:30
PCoder
081f81c41c Begin handling click events 2020-06-19 13:13:26 +05:30
PCoder
ffae844ee5 Merge remote-tracking branch 'mainRepo/master' into 7894/show-one-time-payment-invoices 2020-06-19 12:40:52 +05:30
PCoder
1ce28964a6 Merge remote-tracking branch 'origin/master' into 7894/show-one-time-payment-invoices 2020-06-19 12:40:18 +05:30
PCoder
bc69cc49e5 Move opennebula specific code to celery to make it asynchronous 2020-06-11 15:20:42 +05:30
PCoder
a52215bb56 Update Changelog 2020-06-11 11:42:23 +05:30
PCoder
eadbebb796 Update Changelog for 2.11 2020-06-11 11:34:56 +05:30
PCoder
495e7d4022 Fix wrong constant name
settings.LDAP_MAX_UID_PATH -> settings.LDAP_MAX_UID_FILE_PATH
2020-06-11 11:29:53 +05:30
PCoder
81eee87fb9 Update Changelog for 2.10.8 2020-06-10 13:45:46 +05:30
PCoder
af36a49366 Revert back errors 2020-06-10 13:36:33 +05:30
PCoder
4bff49dab6 Refactor polling time to terminate VM 2020-06-10 12:27:59 +05:30
PCoder
6131270b1d Update Changelog for 2.10.7 2020-05-25 11:44:07 +05:30
PCoder
17a8efb0b6 Fix variable name 2020-05-25 11:33:58 +05:30
a665dbf9c8 Merge branch '8044/fix-modified-vm-templates' into 'master'
Handle updated templates

See merge request ungleich-public/dynamicweb!736
2020-05-25 07:52:35 +02:00
PCoder
c1473fa374 Handle updated templates 2020-05-25 11:10:41 +05:30
PCoder
ac1170a0f1 Remove wrong IL country code in vat_rates 2020-04-21 00:03:13 +05:30
PCoder
f9906781ba Remove wrong Ireland country code in vat rates 2020-04-21 00:02:21 +05:30
PCoder
7db3dc4222 Fix circular imports and correct hosting order link 2020-04-15 17:36:40 +05:30
PCoder
f089892c90 Remove duplicated html + add href link to order 2020-04-10 20:02:12 +05:30
PCoder
a395b7a4a6 Do not update ho -> doing so, crashes 2020-04-10 18:44:45 +05:30
PCoder
8443e03b1f Add ho values 2020-04-07 19:33:13 +05:30
PCoder
3aff4bb69a Fix bug 2020-04-07 19:22:45 +05:30
PCoder
84c3db7e52 Pass HostingOrder instance 2020-04-07 19:21:47 +05:30
PCoder
d35403311f Fix bug fetching variables 2020-04-07 19:16:55 +05:30
PCoder
8a3fa667a0 Fix bug 2020-04-07 18:57:20 +05:30
PCoder
132a5112fd Fix getting id 2020-04-07 18:46:05 +05:30
PCoder
b01f12c9ec Fix passing params to template filter 2020-04-07 18:41:43 +05:30
PCoder
4869fd51df Change to filter from simple 2020-04-07 18:35:25 +05:30
PCoder
4435eef077 Chanage tag to simple 2020-04-01 17:26:12 +05:30
PCoder
343a9440f0 Load invs_charge context from user's charges 2020-04-01 17:09:02 +05:30
PCoder
27aa0ea595 Create another div for onetime charges 2020-04-01 17:08:22 +05:30
PCoder
70264d592d Put contents for subscription in a div of its own 2020-04-01 17:07:44 +05:30
PCoder
d0be07ecd5 Add custom_tag get_line_item_from_hosting_order_charge 2020-04-01 17:07:10 +05:30
PCoder
cec7938c9c Add tabs for one time payments 2020-04-01 11:30:27 +05:30
PCoder
7072420ea5 Update Changelog for 2.10.6 2020-03-25 18:55:49 +05:30
1986978f9b Merge branch 'bugfix/nonetype-in-discount-name' into 'master'
Filter out None case for discount's name

See merge request ungleich-public/dynamicweb!735
2020-03-25 14:23:29 +01:00
PCoder
cb3ff73100 Filter out None case for discount's name 2020-03-25 18:47:00 +05:30
PCoder
46c33cf107 Update price to 11.5 CHF per month in intro emails 2020-03-24 12:34:26 +05:30
58680c1647 Merge branch 'introduce_base_price' into 'master-2.10.3b'
Introduce base price

See merge request ungleich-public/dynamicweb!734
2020-03-24 07:27:55 +01:00
PCoder
9d96ecefea Remove unwanted import 2020-03-18 12:44:18 +05:30
PCoder
580960548e Merge tag '2.10.2' into branch-2.10.3b
Introduce base price for VMs and let admins add stripe_coupon_id
2020-03-18 12:05:57 +05:30
PCoder
f539967a22 Set in_ldap even when resetting dummy credential 2020-03-06 00:01:27 +05:30
PCoder
b076debfee Update Changelog for 2.10.3b 2020-03-05 22:41:15 +05:30
PCoder
d21c5837f7 Change ldap credentials which was set to dummy during migration 2020-03-05 22:40:59 +05:30
PCoder
b9096de386 More replacements: use username instead of emails 2020-03-05 18:17:14 +05:30
PCoder
b44a7f98b5 Use username instead of email when creating VM 2020-03-05 15:55:44 +05:30
PCoder
e2c86116b2 Use username for opennebula related tasks 2020-03-05 15:27:18 +05:30
PCoder
45af92e049 Update Changelog for 2.10.2b 2020-02-25 19:40:10 +05:30
PCoder
42fb55dee1 Don't use encoded bytes and its str representation as uid
(Refer redmine #7764)
2020-02-25 19:19:40 +05:30
PCoder
2cbf146ebc Change button to a href 2020-02-25 14:40:54 +05:30
PCoder
c058138044 Update Changelog for 2.10.2 2020-02-04 17:07:14 +05:30
PCoder
f45f8dd51f Remove unused method round_up 2020-02-04 11:27:59 +05:30
PCoder
dd2eae68e6 Use math round to round amount to 2 decimal places 2020-02-04 11:20:17 +05:30
PCoder
e322e58246 Use appropriate stripe_coupon_id 2020-02-04 09:06:10 +05:30
PCoder
3ca1a45217 Add stripe_coupon_id to VMPricing 2020-02-04 08:57:54 +05:30
PCoder
c315030b06 Add vm base price to missing places 2020-02-03 12:29:14 +05:30
PCoder
e6de90e431 Set vm base price in js also 2020-02-03 12:07:50 +05:30
PCoder
00b434efb9 Read VM_BASE_PRICE from env 2020-02-03 11:37:30 +05:30
PCoder
5d977f32d3 Update Changelog for 2.10.1 2020-02-02 22:52:11 +05:30
PCoder
3061c1483c Switch bold between Recurring text and value 2020-02-02 22:37:29 +05:30
PCoder
f68549d80d Add DE translations by moep 2020-02-02 22:33:13 +05:30
PCoder
b607575c8c Switch bold for description title and description text 2020-02-02 22:29:01 +05:30
PCoder
368db61a2f Remove padding left 2020-02-02 22:11:26 +05:30
PCoder
359a633047 Fix mobile view 2020-02-02 22:00:22 +05:30
PCoder
1630dc195b Reduce header font size 2020-02-02 21:52:48 +05:30
PCoder
f0f8af2367 Change col-sm-8 to col-sm-9 to spread content further on desktop view 2020-02-02 21:39:51 +05:30
PCoder
a5d393ad20 Right align prices 2020-02-02 13:21:01 +05:30
PCoder
70a3620598 Fix getting product from plan_name 2020-02-02 13:09:27 +05:30
PCoder
42a4a77c02 Show product name 2020-02-02 12:42:19 +05:30
PCoder
e094930d6e Show product name in invoices list if product is not a VM 2020-02-02 12:39:19 +05:30
PCoder
c0683d9f53 Show prices to two decimal places 2020-02-02 10:57:14 +05:30
PCoder
fc8c4579fb Show prices to two decimals 2020-02-02 10:41:51 +05:30
PCoder
9f86f44569 Add missing divs 2020-02-02 10:29:33 +05:30
PCoder
ffde015c31 Fix divs 2020-02-02 10:26:13 +05:30
PCoder
c765698a0f Add two columns for pricing 2020-02-02 10:16:45 +05:30
PCoder
44dee625b4 Add some DE translations 2020-02-01 20:24:55 +05:30
PCoder
88a39ef85a Update Changelog for 2.10 2020-02-01 15:23:42 +05:30
PCoder
8fe689d993 Update some DE translations 2020-02-01 15:23:26 +05:30
PCoder
b103cff0a6 Dont round_up discount (is obtained from subtraction) 2020-02-01 13:54:29 +05:30
PCoder
c43afb7c59 Use round_up method 2020-02-01 13:47:29 +05:30
PCoder
2058c660c0 Retrieve only one invoice 2020-02-01 13:15:03 +05:30
PCoder
23b25002ae Take users to invoice url instead of orders
Orders need a VAT alignment fix
2020-02-01 13:00:10 +05:30
PCoder
b645f9894b Open invoice in a new window 2020-02-01 12:25:56 +05:30
PCoder
9d21181073 Get stripe invoice obj correctly 2020-02-01 12:23:47 +05:30
PCoder
d8482c52f9 Get invoice correctly 2020-02-01 12:20:13 +05:30
PCoder
918d2b17e1 Change invoice url 2020-02-01 12:13:56 +05:30
PCoder
e6f00abd71 Do not round 2020-02-01 09:17:24 +05:30
PCoder
838163bd59 Make total price consistent 2020-02-01 09:15:12 +05:30
PCoder
b1acd3f25b Convert VAT percent to CHF before calculations 2020-02-01 09:02:17 +05:30
PCoder
f4393426d3 Fix proper VAT values 2020-02-01 08:53:24 +05:30
PCoder
7a9b315e2e Add media query for device width less than 368px 2020-01-30 12:33:47 +05:30
PCoder
3141dc2793 Round price_with_vat 2020-01-30 11:52:29 +05:30
PCoder
3d51e4fe32 Update alignments 2020-01-30 11:39:54 +05:30
PCoder
f546c5cb4f Increase content space in order detail desktop view 2020-01-29 17:38:44 +05:30
PCoder
4128aeb64d Add line height for final price 2020-01-29 17:21:59 +05:30
PCoder
8ee4081f60 Fix round up calculations 2020-01-29 16:58:47 +05:30
PCoder
ad606c2c55 Correct calculation of total price 2020-01-29 16:50:05 +05:30
PCoder
a81fdc8ec1 Revert back to original vat calculation 2020-01-29 16:45:07 +05:30
PCoder
6132638faa Use correct vat 2020-01-29 16:32:07 +05:30
PCoder
48cc2b4939 Fix discount amount calculation after VAT 2020-01-29 16:19:56 +05:30
PCoder
1f79ccd490 Convert discount amount into CHF 2020-01-29 16:15:28 +05:30
PCoder
b8eca59e0d Fix design for showing prices excl and incl vat and discount 2020-01-29 16:04:03 +05:30
PCoder
970834cc38 Add parameters needed for displaying prices after discount, and vat 2020-01-29 16:03:13 +05:30
PCoder
24740438f7 get_vm_price_for_given_vat: Also return discount amount with vat 2020-01-29 16:02:26 +05:30
PCoder
cde6c51d4b Reset vat_validation_status when on payment page 2020-01-26 10:06:31 +05:30
PCoder
d1fd57b730 Correct the way we get amount from discount 2020-01-24 15:26:41 +05:30
PCoder
156930ab26 Add price_after_discount explicitly for showing in template 2020-01-24 15:24:46 +05:30
PCoder
112f3e17a9 Convert to decimal for type consistency 2020-01-24 15:03:07 +05:30
PCoder
38550ea75c Rearrange the way we show VAT and discount calculations 2020-01-24 15:00:16 +05:30
PCoder
e01b27835e Make subscription exclusive of VAT + Add VAT separately to subscription 2020-01-24 14:11:55 +05:30
PCoder
ec00785068 Update get_stripe_plan_id to make it compatible with excl_vat 2020-01-24 14:10:56 +05:30
PCoder
399f9ed6c9 Adjust hosting VM buy flow 2020-01-23 16:37:35 +05:30
PCoder
a82c4d556c Update Changelog for 2.9.5 2020-01-20 13:25:49 +05:30
PCoder
0b2a305f57 Fix pep warning 2020-01-20 12:09:49 +05:30
PCoder
a00a9f6ff0 Show invoices directly from stripe 2020-01-20 12:07:32 +05:30
PCoder
fd4f61bc5c Update Changelog for 2.9.4 2020-01-10 20:16:16 +05:30
PCoder
9212c02cd7 Check vat_validation_status exists in dict before using it 2020-01-10 19:47:49 +05:30
PCoder
5fe1c21b57 Update Changelog for 2.9.3 2020-01-05 12:56:17 +05:30
PCoder
f762cbe58c Update Changelog 2020-01-05 10:00:57 +05:30
PCoder
4b8b0b0540 Add stripe tax rate migration 2020-01-05 09:59:23 +05:30
PCoder
5468d5436c Add StripeTaxRate model 2020-01-05 09:57:54 +05:30
PCoder
96e50ddc8a Add vat rates for CY, IL and LI manually 2020-01-04 22:47:22 +05:30
PCoder
235904d784 Update Changelog for 2.9.2 2020-01-02 12:58:53 +05:30
PCoder
ceb7f9b0e6 Revert back to email 2020-01-02 12:34:42 +05:30
PCoder
7d7bd60a7f Error emails list is already a list 2020-01-02 12:27:26 +05:30
PCoder
b4a3c5e277 Send VM deleted message to error mail list 2020-01-02 12:17:57 +05:30
PCoder
5f81bc9091 Improve admin email for VM terminate 2020-01-02 12:07:55 +05:30
PCoder
690952156d Convert to str before joining 2020-01-01 02:04:55 +05:30
PCoder
f4e84f62a4 Save billing address only if billing_address exists 2020-01-01 01:28:35 +05:30
PCoder
0d27bac3a8 Update Changelog for 2.9.1 2020-01-01 00:54:03 +05:30
PCoder
251676ead8 Fix getting StripeCustomer from stripe_id 2020-01-01 00:52:11 +05:30
PCoder
2acb1fb418 Remove duplicate 2020-01-01 00:37:36 +05:30
PCoder
7130df9fd4 Update Changelog (add notes for deployment) 2020-01-01 00:36:22 +05:30
PCoder
18b04a70e6 Update Changelog for 2.9 2020-01-01 00:33:57 +05:30
PCoder
ed1a4fc1a6 Merge remote-tracking branch 'mainRepo/master' into feature/VAT_number 2020-01-01 00:21:35 +05:30
PCoder
0bca5113ca Fix unwanted char 2020-01-01 00:10:56 +05:30
PCoder
efaf75615b Send email to admin on VAT number update 2020-01-01 00:01:44 +05:30
PCoder
d4bfcbef47 Use message as returned by stripe 2019-12-31 22:55:53 +05:30
PCoder
6674e70ded Compare vat numbers only 2019-12-31 22:54:12 +05:30
PCoder
8f2bd568db Restore billing address if VAT number is not valid 2019-12-31 22:53:32 +05:30
PCoder
0695d68903 Create StripeCustomer if not already created 2019-12-31 22:52:49 +05:30
PCoder
6ac6db8212 Get the last user billing address as the default address 2019-12-31 22:52:24 +05:30
PCoder
7423a80670 Use correct variable 2019-12-31 22:50:46 +05:30
PCoder
ca5724f10f Fix error message 2019-12-31 18:36:38 +05:30
PCoder
2378410f2d Compare country for creating new tax id 2019-12-31 18:22:57 +05:30
PCoder
9078e46196 Add log 2019-12-31 18:08:09 +05:30
PCoder
3ca7e89f4f Show VAT for eu countries only 2019-12-31 17:28:11 +05:30
PCoder
aba3092207 Ignore spaces, hyphens and dots in vat number comparison 2019-12-31 15:57:50 +05:30
PCoder
7949ab274e Impove logging 2019-12-31 15:43:43 +05:30
PCoder
b567b01362 For CH we don't care whether VAT is validated or not 2019-12-26 20:51:11 +05:30
PCoder
7397be98a5 Remove message tags printed mistakenly 2019-12-26 20:40:50 +05:30
PCoder
398a255965 Change condition so as to show error messages in red 2019-12-26 20:36:54 +05:30
PCoder
3202c83c68 Show error messages in red 2019-12-26 20:32:01 +05:30
PCoder
2a760639f6 Set validation status to empty on error 2019-12-26 20:27:15 +05:30
PCoder
c142d743d1 Show only error message 2019-12-26 20:25:07 +05:30
PCoder
74921fcd4a Send the same error message as forwarded by Stripe 2019-12-26 20:21:01 +05:30
PCoder
12975565a5 More bugfix 2019-12-26 20:09:28 +05:30
PCoder
d62986c91f Bugfix: use correct total price after discount 2019-12-26 20:05:55 +05:30
PCoder
fcdabd8dc3 Set vat validation status in more places 2019-12-26 19:55:28 +05:30
PCoder
a5c7865811 Save validation status 2019-12-26 19:37:57 +05:30
PCoder
69996f536b Add vat_validation_status 2019-12-26 19:35:50 +05:30
PCoder
7eed04ec73 Remove one log too many 2019-12-26 19:16:26 +05:30
PCoder
cbf2f05d70 Use the latest billing address as the default one 2019-12-26 18:54:17 +05:30
PCoder
9e87fa76c3 More logging 2019-12-26 18:47:40 +05:30
PCoder
f2d738ae62 Improve logging format 2019-12-26 18:40:06 +05:30
PCoder
c6147c887c More logging + improve ba string repr 2019-12-26 18:34:26 +05:30
PCoder
064ea5be2f More logging 2019-12-26 18:26:30 +05:30
PCoder
32d9f06c18 Add logger messages 2019-12-26 18:22:08 +05:30
PCoder
6d0a7f7049 Improve stripe_customer_id 2019-12-26 18:06:22 +05:30
PCoder
5ab0bf6993 Retrieve tax id if exists before creating a new one 2019-12-26 14:46:16 +05:30
PCoder
ad52338653 Save billing addresses 2019-12-26 14:27:16 +05:30
PCoder
d8c03a4364 Add missing param 2019-12-26 14:21:18 +05:30
PCoder
9aff248d31 Use correct billingaddress 2019-12-26 14:19:07 +05:30
PCoder
364f5599e6 Correct the way we show vat error 2019-12-26 14:08:53 +05:30
PCoder
ec5bfb18b3 Replace parenthesis is template 2019-12-26 14:05:56 +05:30
PCoder
c3b22992ea Fix wrong elif syntax 2019-12-26 14:00:27 +05:30
PCoder
2038d719f0 Show status icon for pending and verified only 2019-12-26 13:58:50 +05:30
PCoder
b284ed70a6 Show error elegantly 2019-12-26 13:56:31 +05:30
PCoder
7eff6fc92c Use correct field 2019-12-26 13:52:14 +05:30
PCoder
262bf3e2f7 Force VAT validation on each save 2019-12-26 13:49:18 +05:30
PCoder
99e70d95c4 VAT number validation in settings 2019-12-26 13:31:15 +05:30
PCoder
7d9ab322c9 Remove vat_validate_on session var 2019-12-26 12:38:00 +05:30
PCoder
fe6ade38eb Update vat_number validated field based on tax_id + save 2019-12-26 11:54:54 +05:30
PCoder
242dbb2479 Bugfix: pass param 2019-12-26 11:50:15 +05:30
PCoder
833dc9bdcb Change stripe_tax_id field to char + regenerate migrations 2019-12-26 11:41:29 +05:30
PCoder
9310f72cf9 Save tax id in billing address even if no StripeCustomer 2019-12-26 11:30:01 +05:30
PCoder
74d1bbb6d3 Add missing customer save call 2019-12-26 11:09:14 +05:30
PCoder
6fd0659c88 Another missing param 2019-12-26 11:02:58 +05:30
PCoder
4560c8bf83 Bugfix: Pass parameter name 2019-12-26 11:00:24 +05:30
PCoder
0e40ca6044 Also validate vat in order confirmation get 2019-12-26 10:55:01 +05:30
PCoder
c393902396 Save address form in payment post itself 2019-12-26 10:54:26 +05:30
PCoder
825e716625 Update tax_id fields of billing_addresses which belong to supplied user only 2019-12-26 10:51:50 +05:30
PCoder
0d208d2bd9 Take billing_address_id in validate_vat_number 2019-12-26 10:50:53 +05:30
PCoder
52201c0aa1 Clear billing_address_id session var also 2019-12-26 10:49:45 +05:30
PCoder
ca128dd8c4 Fix str formatting 2019-12-26 08:06:27 +05:30
PCoder
0b8315ca76 Remove wrong filter 2019-12-25 23:20:48 +05:30
PCoder
4a40438edc Also handle ch_vat 2019-12-25 23:10:18 +05:30
PCoder
908c2e055c Also clear vat_validation_status from session 2019-12-25 22:48:57 +05:30
PCoder
4491a52bd9 Check if vat_validation_status is in session 2019-12-25 22:45:55 +05:30
PCoder
b15ece7088 Validate VAT number only if it is set 2019-12-25 22:40:57 +05:30
PCoder
3b654f1c49 Correct++ 2019-12-25 22:35:41 +05:30
PCoder
752b61a852 Correct the way to get the first object 2019-12-25 22:33:56 +05:30
PCoder
110459b38d Show vat error in payment page 2019-12-25 22:27:14 +05:30
PCoder
de0fe77779 Bugfix: pass correct param 2019-12-25 22:22:08 +05:30
PCoder
785f99311d Handle invalid VAT number case 2019-12-25 21:02:43 +05:30
PCoder
8cc766b62f Update stripe to 2.41.0 to support tax id 2019-12-25 20:45:38 +05:30
PCoder
1b243822a9 Bugfix: remove unnecessary self param 2019-12-25 20:20:43 +05:30
PCoder
1400d27afa Handle tax_id creation 2019-12-25 20:11:15 +05:30
PCoder
8996254212 Save stripe tax id 2019-12-25 18:44:43 +05:30
PCoder
80f01aec07 Add steps to be followed for tax_id updated webhook 2019-12-25 12:36:47 +05:30
PCoder
127c83059f Add stub files from webhook app 2019-12-25 12:20:26 +05:30
PCoder
772c7557e3 Update Stripe version to 2.24.1 (supports webhook) 2019-12-25 12:09:43 +05:30
PCoder
55979f3701 Add webhook to installed apps + introduce some webhook constants 2019-12-25 12:09:04 +05:30
PCoder
f6832c090e Add webhooks/views.py 2019-12-25 12:07:42 +05:30
PCoder
cb065d36df Add webhook handling url 2019-12-25 12:07:09 +05:30
PCoder
50043f8283 Add webhook management command
Handles managing stripe webhooks
2019-12-25 12:03:58 +05:30
PCoder
c6db34efdd Add DateTimeField vat_number_validated_on 2019-12-25 11:00:04 +05:30
PCoder
92570ada7f Add VAT number to order detail (hosting) 2019-12-25 10:37:45 +05:30
PCoder
8ebd12c420 Add missing vat_number while saving billing address 2019-12-25 10:32:37 +05:30
PCoder
e48ae6a39d Update migration to make vat_number non mandatory 2019-12-25 08:31:40 +05:30
PCoder
6eb4b03afe Merge remote-tracking branch 'origin/master' into feature/VAT_number 2019-12-25 08:15:22 +05:30
PCoder
8409acf02d Add missing vat_number field 2019-12-21 10:10:32 +05:30
PCoder
32cfdea68c Add missing vat_number field to user billing address 2019-12-21 10:05:58 +05:30
PCoder
e3078f3ea9 Add migration 2019-12-21 08:52:43 +05:30
PCoder
2d66ae6783 Improve BillingAddress string representation
Include VAT number if available
2019-12-21 08:52:09 +05:30
PCoder
202a514b1b Set empty default value for vat_number 2019-12-21 08:51:43 +05:30
PCoder
b919b6cfbd Fix wrong indentation 2019-12-21 08:51:11 +05:30
PCoder
f566aa8a2e Make VAT number a part of billing address 2019-12-21 08:43:34 +05:30
PCoder
c9de757bc7 Merge remote-tracking branch 'mainRepo/master' into feature/VAT_number 2019-12-21 08:19:23 +05:30
PCoder
24edf05e7a Save vat_number after payment is submitted 2019-12-17 23:57:15 +05:30
PCoder
6ea486b527 Initialize vat number in payment forms 2019-12-17 23:48:05 +05:30
PCoder
568d874476 Add initial value for the vat_number field in the settings 2019-12-17 23:32:49 +05:30
PCoder
5e97d70a5e Save VAT number 2019-12-17 22:52:22 +05:30
PCoder
0f3acf5db4 Change order_detail and invoice_detail templates to show VAT number 2019-12-17 22:51:54 +05:30
PCoder
cdaf498487 Remove default for CharField (introduced unwantedly) 2019-12-17 22:09:34 +05:30
PCoder
3efd6087e2 Add vat_number migration file 2019-12-17 22:08:55 +05:30
PCoder
e11882685f Add vat_number field to CustomUser 2019-12-17 22:07:57 +05:30
PCoder
33120d14f3 Modify billing address form and signup forms to include vat_number 2019-12-17 21:35:02 +05:30
PCoder
490ceec47d Add france metropolitan vat rate 2019-12-17 21:34:17 +05:30
75 changed files with 3718 additions and 1013 deletions

1
.dockerignore Normal file
View file

@ -0,0 +1 @@
.git

View file

@ -1,3 +1,96 @@
3.2: 2021-02-07
* 8816: Update order confirmation text to better prepared for payment dispute
* supportticket#22990: Fix: can't add a deleted card
3.1: 2021-01-11
* 8781: Fix error is setting a default card (MR!746)
3.0: 2021-01-07
* 8393: Implement SCA for stripe payments (MR!745)
* 8691: Implment check_vm_templates management command (MR!744)
2.14: 2020-12-07
* 8692: Create a script that fixes django db for the order after celery error (MR!743)
2.13: 2020-12-02
* 8654: Fix 500 error on invoices list for the user contact+devuanhosting.com@virus.media (MR!742)
* 8593: Escape user's ssh key in xml-rpc call to create VM (MR!741)
2.12.1: 2020-07-21
* 8307: Introduce "Exclude vat calculations" for Generic Products (MR!740)
* Change DE VAT rate to 16% from 19% (MR!739)
2.12: 2020-06-23
* 7894: Show one time payment invoices (MR!738)
2.11: 2020-06-11
* Bugfix: Correct the wrong constant name (caused payment to go thru and showing error and VMs not instantiated)
2.10.8: 2020-06-10
* #8102: Refactor MAX_TIME_TO_WAIT_FOR_VM_TERMINATE to increase time to poll whether VM has been terminated or not (MR!737)
2.10.7: 2020-05-25
* Bugfix: Handle VM templates deleted in OpenNebula but VM instances still existing (MR!736)
Notes for deployment:
When deploying define a UPDATED_TEMPLATES string represented dictionary value in .env
```
# Represents Template Ids that were
# deleted and the new template Id to look for the template
# definition
UPDATED_TEMPLATES="{1: 100}"
```
2.10.6: 2020-03-25
* Bugfix: Handle Nonetype for discount's name (MR!735)
2.10.5: 2020-03-17
* Introduce base price for VMs and let admins add stripe_coupon_id (MR!730)
Notes for deployment:
1. Add env variable `VM_BASE_PRICE`
2. Migrate datacenterlight app. This introduces the stripe_coupon_code field in the VMPricing.
3. Create a coupon in stripe with the desired value and note down the stripe's coupon id
4. Update the discount amount and set the corresponding coupon id in the admin
2.10.3b: 2020-03-05
* #7773: Use username for communicating with opennebula all the time
2.10.2b: 2020-02-25
* #7764: Fix uid represented as bytestring
* #7769: [hosting] ssh private key download feature does not work well on Firefox
2.10.1: 2020-02-02:
* Changes the pricing structure of generic products into the pre vat and with vat (like that for VM)
* Shows product name (if exists) in the invoices list if it belongs to a generic product
* Small bugfixes (right alignment of price in the invoice list, show prices with 2 decimal places etc)
2.10: 2020-02-01
* Feature: Introduce new design to show VAT exclusive/VAT inclusive pricing together
* Feature: Separate VAT and discount in Stripe
* Feature: Show Stripe invoices until we have a better way of showing them elegantly
* Bugfix: Fix bug where VAT is set to 0 because user set a valid VAT number before but later chose not to use any VAT number
2.9.5: 2020-01-20
* Feature: Show invoices directly from stripe (MR!727)
2.9.4: 2020-01-10
* Bugfix: Buying VPN generic item caused 500 error
2.9.3: 2020-01-05
* Feature: Add StripeTaxRate model to save tax rates created in Stripe
* Bugfix: Add vat rates for CY, IL and LI manually
2.9.2: 2020-01-02
* Bugfix: Improve admin email for terminate vm (include subscription details and subscription amount) (MR!726)
2.9.1: 2019-12-31
* Bugfix: Error handling tax_id updated webhook
2.9: 2019-12-31
* Feature: Enable saving user's VAT Number and validate it (MR!725)
Notes for deployment:
1. Migrate db for utils app
./manage.py migrate utils
2. Uninstall old version and install a more recent version of stripe
```
source venv/bin/activate
./manage.py shell
pip uninstall stripe
pip install stripe==2.41.0
```
3. Create tax id updated webhook
```
./manage.py webhook --create \
--webhook_endpoint https://datacenterlight.ch/en-us/webhooks/ \
--events_csv customer.tax_id.updated
```
4. From the secret obtained in 3, setup an environment variable
```
WEBHOOK_SECRET='whsec......'
```
5. Deploy
2.8.2: 2019-12-24
* Bugfix: [dcl calculator plugin] Set the POST action url explicitly
2.8.1: 2019-12-24

18
Dockerfile Normal file
View file

@ -0,0 +1,18 @@
FROM python:3.10.0-alpine3.15
WORKDIR /usr/src/app
RUN apk add --update --no-cache \
git \
build-base \
openldap-dev \
python3-dev \
libpq-dev \
&& rm -rf /var/cache/apk/*
# FIX https://github.com/python-ldap/python-ldap/issues/432
RUN echo 'INPUT ( libldap.so )' > /usr/lib/libldap_r.so
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY ./ .

View file

@ -14,6 +14,12 @@ help:
@echo ' make rsync_upload '
@echo ' make install_debian_packages '
buildimage:
docker build -t dynamicweb:$$(git describe) .
releaseimage: buildimage
./release.sh
collectstatic:
$(PY?) $(BASEDIR)/manage.py collectstatic

View file

@ -1,5 +1,6 @@
from cms.plugin_base import CMSPluginBase
from cms.plugin_pool import plugin_pool
from django.conf import settings
from .cms_models import (
DCLBannerItemPluginModel, DCLBannerListPluginModel, DCLContactPluginModel,
@ -100,6 +101,7 @@ class DCLCalculatorPlugin(CMSPluginBase):
vm_type=instance.vm_type
).order_by('name')
context['instance'] = instance
context['vm_base_price'] = settings.VM_BASE_PRICE
context['min_ram'] = 0.5 if instance.enable_512mb_ram else 1
return context

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-12-09 12:13+0000\n"
"POT-Creation-Date: 2021-02-07 11:10+0000\n"
"PO-Revision-Date: 2018-03-30 23:22+0000\n"
"Last-Translator: b'Anonymous User <coder.purple+25@gmail.com>'\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -144,9 +144,8 @@ msgid ""
"the heart of Switzerland."
msgstr "Bei uns findest Du die günstiges VMs aus der Schweiz."
msgid "Try now, order a VM. VM price starts from only 10.5 CHF per month."
msgstr "Unser Angebot beginnt bei 10.5 CHF pro Monat. Probier's jetzt aus!"
msgid "Try now, order a VM. VM price starts from only 11.5 CHF per month."
msgstr "Unser Angebot beginnt bei 11.5 CHF pro Monat. Probier's jetzt aus!"
msgid "ORDER VM"
msgstr "VM BESTELLEN"
@ -407,6 +406,19 @@ msgstr "Datum"
msgid "Billed to"
msgstr "Rechnungsadresse"
msgid "VAT Number"
msgstr "MwSt-Nummer"
msgid "Your VAT number has been verified"
msgstr "Deine MwSt-Nummer wurde überprüft"
msgid ""
"Your VAT number is under validation. VAT will be adjusted, once the "
"validation is complete."
msgstr ""
"Deine MwSt-Nummer wird derzeit validiert. Die MwSt. wird angepasst, sobald "
"die Validierung abgeschlossen ist."
msgid "Payment method"
msgstr "Bezahlmethode"
@ -419,64 +431,63 @@ msgstr "Bestellungsübersicht"
msgid "Product"
msgstr "Produkt"
msgid "Price"
msgstr "Preise"
msgid "VAT for"
msgstr "MwSt für"
msgid "Total Amount"
msgstr "Gesamtsumme"
msgid "Amount"
msgstr "Betrag"
msgid "Description"
msgstr "Beschreibung"
msgid "Recurring"
msgstr "Wiederholend"
msgid "Subtotal"
msgstr "Zwischensumme"
msgid "Price Before VAT"
msgstr "Preis ohne MwSt."
#, fuzzy, python-format
#| msgid ""
#| "By clicking \"Place order\" this plan will charge your credit card "
#| "account with %(total_price)s CHF/month"
msgid ""
"By clicking \"Place order\" this plan will charge your credit card account "
"with %(total_price)s CHF/year"
msgstr ""
"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit %(total_price)s "
"CHF pro Jahr belastet"
msgid "Pre VAT"
msgstr "Exkl. MwSt."
msgid "VAT for"
msgstr "MwSt für"
msgid "Your Price in Total"
msgstr "Dein Gesamtpreis"
#, python-format
msgid ""
"By clicking \"Place order\" this plan will charge your credit card account "
"with %(total_price)s CHF/month"
" By clicking \"Place order\" you agree to our <a href=\"https://"
"datacenterlight.ch/en-us/cms/terms-of-service/\">Terms of Service</a> and "
"this plan will charge your credit card account with %(total_price)s CHF/year"
msgstr ""
"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit %(total_price)s "
"CHF pro Monat belastet"
#, fuzzy, python-format
#| msgid ""
#| "By clicking \"Place order\" this payment will charge your credit card "
#| "account with a one time amount of %(total_price)s CHF"
msgid ""
"By clicking \"Place order\" this payment will charge your credit card "
"account with a one time amount of %(total_price)s CHF"
msgstr ""
"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit "
"%(vm_total_price)s CHF pro Monat belastet"
"Indem Du auf \"Bestellung aufgeben\" klickst, erklärst Du dich mit unseren <a href=\"https://"
"datacenterlight.ch/en-us/cms/terms-of-service/\">Nutzungsbedingungen</a> einverstanden und Dein Kreditkartenkonto wird mit %(total_price)s CHF/Jahr belastet."
#, python-format
msgid ""
"By clicking \"Place order\" this plan will charge your credit card account "
"with %(vm_total_price)s CHF/month"
"\n"
" By clicking \"Place order\" you agree to "
"our <a href=\"https://datacenterlight.ch/en-us/cms/terms-of-service/\">Terms "
"of Service</a> and this plan will charge your credit card account with "
"%(total_price)s CHF/month"
msgstr ""
"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit "
"%(vm_total_price)s CHF pro Monat belastet"
"\n"
"Indem Du auf \"Bestellung aufgeben\" klickst, erklärst Du dich mit unseren <a href=\"https://"
"datacenterlight.ch/en-us/cms/terms-of-service/\">Nutzungsbedingungen</a> einverstanden und Dein Kreditkartenkonto wird mit %(total_price)s CHF/Monat belastet."
#, python-format
msgid ""
"By clicking \"Place order\" you agree to our <a href=\"https://"
"datacenterlight.ch/en-us/cms/terms-of-service/\">Terms of Service</a> and "
"this plan will charge your credit card account with %(total_price)s CHF"
msgstr ""
"Indem Du auf \"Bestellung aufgeben\" klickst, erklärst Du dich mit unseren <a href=\"https://"
"datacenterlight.ch/de/cms/terms-of-service/\">Nutzungsbedingungen</a> einverstanden und Dein Kreditkartenkonto wird mit %(total_price)s CHF belastet."
#, python-format
msgid ""
"By clicking \"Place order\" you agree to our <a href=\"https://"
"datacenterlight.ch/en-us/cms/terms-of-service/\">Terms of Service</a> and "
"this plan will charge your credit card account with %(vm_total_price)s CHF/"
"month"
msgstr ""
"Indem Du auf \"Bestellung aufgeben\" klickst, erklärst Du dich mit unseren <a href=\"https://"
"datacenterlight.ch/de/cms/terms-of-service/\">Nutzungsbedingungen</a> einverstanden und Dein Kreditkartenkonto wird mit %(vm_total_price)s CHF/Monat belastet"
msgid "Place order"
msgstr "Bestellen"
@ -575,6 +586,9 @@ msgstr "Unser Angebot beginnt bei 15 CHF pro Monat. Probier's jetzt aus!"
msgid "Actions speak louder than words. Let's do it, try our VM now."
msgstr "Taten sagen mehr als Worte Teste jetzt unsere VM!"
msgid "See Invoice"
msgstr "Siehe Rechnung"
msgid "Invalid number of cores"
msgstr "Ungültige Anzahle CPU-Kerne"
@ -592,16 +606,22 @@ msgid "Incorrect pricing name. Please contact support{support_email}"
msgstr ""
"Ungültige Preisbezeichnung. Bitte kontaktiere den Support{support_email}"
#, python-brace-format
msgid "{user} does not have permission to access the card"
msgstr "{user} hat keine Erlaubnis auf diese Karte zuzugreifen"
msgid "An error occurred. Details: {}"
msgstr "Ein Fehler ist aufgetreten. Details: {}"
msgid "Confirm Order"
msgstr "Bestellung Bestätigen"
#, fuzzy
#| msgid "Thank you!"
msgid "Thank you !"
msgstr "Vielen Dank!"
msgid "Your product will be provisioned as soon as we receive the payment."
msgstr ""
#, 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 "Error."
msgstr ""
@ -612,10 +632,21 @@ msgstr ""
"Es ist ein Fehler bei der Zahlung betreten. Du wirst nach dem Schliessen vom "
"Popup zur Bezahlseite weitergeleitet."
#, python-brace-format
msgid "An error occurred while associating the card. Details: {details}"
msgid "Thank you for the order."
msgstr "Danke für Deine Bestellung."
msgid ""
"Your product will be provisioned as soon as we receive a payment "
"confirmation from Stripe. We will send you a confirmation email. You can "
"always contact us at support@datacenterlight.ch"
msgstr ""
"Beim Verbinden der Karte ist ein Fehler aufgetreten. Details: {details}"
msgid ""
"Your VM will be up and running in a few moments. We will send you a "
"confirmation email as soon as it is ready."
msgstr ""
"Deine VM ist gleich bereit. Wir senden Dir eine Bestätigungsemail, sobald Du "
"auf sie zugreifen kannst."
msgid " This is a monthly recurring plan."
msgstr "Dies ist ein monatlich wiederkehrender Plan."
@ -655,15 +686,43 @@ msgstr ""
"Du wirst bald eine Bestätigungs-E-Mail über die Zahlung erhalten. Du kannst "
"jederzeit unter info@ungleich.ch kontaktieren."
msgid "Thank you for the order."
msgstr "Danke für Deine Bestellung."
#, python-format
#~ msgid ""
#~ "By clicking \"Place order\" this plan will charge your credit card "
#~ "account with %(total_price)s CHF/month"
#~ msgstr ""
#~ "Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit "
#~ "%(total_price)s CHF pro Monat belastet"
msgid ""
"Your VM will be up and running in a few moments. We will send you a "
"confirmation email as soon as it is ready."
msgstr ""
"Deine VM ist gleich bereit. Wir senden Dir eine Bestätigungsemail, sobald Du "
"auf sie zugreifen kannst."
#, fuzzy, python-format
#~| msgid ""
#~| "By clicking \"Place order\" this payment will charge your credit card "
#~| "account with a one time amount of %(total_price)s CHF"
#~ msgid ""
#~ "By clicking \"Place order\" this payment will charge your credit card "
#~ "account with a one time amount of %(total_price)s CHF"
#~ msgstr ""
#~ "Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit "
#~ "%(vm_total_price)s CHF pro Monat belastet"
#, 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 "Price"
#~ msgstr "Preise"
#~ msgid "Total Amount"
#~ msgstr "Gesamtsumme"
#~ msgid "Amount"
#~ msgstr "Betrag"
#~ msgid "Subtotal"
#~ msgstr "Zwischensumme"
#~ msgid "VAT"
#~ msgstr "Mehrwertsteuer"
@ -676,9 +735,6 @@ msgstr ""
#~ "ausgelöst, nachdem Du die Bestellung auf der nächsten Seite bestätigt "
#~ "hast."
#~ msgid "Card Number"
#~ msgstr "Kreditkartennummer"
#~ msgid ""
#~ "You are not making any payment yet. After placing your order, you will be "
#~ "taken to the Submit Payment Page."
@ -730,9 +786,6 @@ msgstr ""
#~ "Wir werden dann sobald als möglich Ihren Beta-Zugang erstellen und Sie "
#~ "daraufhin kontaktieren.Bis dahin bitten wir Sie um etwas Geduld."
#~ msgid "Thank you!"
#~ msgstr "Vielen Dank!"
#~ msgid "Thank you for order! Our team will contact you via email"
#~ msgstr ""
#~ "Vielen Dank für die Bestellung. Unser Team setzt sich sobald wie möglich "

View file

@ -0,0 +1,65 @@
from django.core.management.base import BaseCommand
from opennebula_api.models import OpenNebulaManager
from datacenterlight.models import VMTemplate
from membership.models import CustomUser
from django.conf import settings
from time import sleep
import datetime
import json
import logging
import os
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = '''Checks all VM templates to find if they can be instantiated'''
def add_arguments(self, parser):
parser.add_argument('user_email', type=str)
def handle(self, *args, **options):
result_dict = {}
user_email = options['user_email'] if 'user_email' in options else ""
if user_email:
cu = CustomUser.objects.get(email=user_email)
specs = {'cpu': 1, 'memory': 1, 'disk_size': 10}
manager = OpenNebulaManager(email=user_email, password=cu.password)
pub_keys = [settings.TEST_MANAGE_SSH_KEY_PUBKEY]
PROJECT_PATH = os.path.abspath(os.path.dirname(__name__))
if not os.path.exists("%s/outputs" % PROJECT_PATH):
os.mkdir("%s/outputs" % PROJECT_PATH)
for vm_template in VMTemplate.objects.all():
vm_name = 'test-%s' % vm_template.name
vm_id = manager.create_vm(
template_id=vm_template.opennebula_vm_template_id,
specs=specs,
ssh_key='\n'.join(pub_keys),
vm_name=vm_name
)
if vm_id and vm_id > 0:
result_dict[vm_name] = "%s OK, created VM %s" % (
'%s %s %s' % (vm_template.opennebula_vm_template_id,
vm_template.name, vm_template.vm_type),
vm_id
)
self.stdout.write(self.style.SUCCESS(result_dict[vm_name]))
manager.delete_vm(vm_id)
else:
result_dict[vm_name] = '''Error creating VM %s, template_id
%s %s''' % (vm_name,
vm_template.opennebula_vm_template_id,
vm_template.vm_type)
self.stdout.write(self.style.ERROR(result_dict[vm_name]))
sleep(1)
date_str = datetime.datetime.strftime(
datetime.datetime.now(), '%Y%m%d%H%M%S'
)
with open("%s/outputs/check_vm_templates_%s.txt" %
(PROJECT_PATH, date_str),
'w',
encoding='utf-8') as f:
f.write(json.dumps(result_dict))
self.stdout.write(self.style.SUCCESS("Done"))

View file

@ -0,0 +1,76 @@
from django.core.management.base import BaseCommand
from datacenterlight.tasks import handle_metadata_and_emails
from opennebula_api.models import OpenNebulaManager
from membership.models import CustomUser
import logging
import json
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = '''Updates the DB after manual creation of VM'''
def add_arguments(self, parser):
parser.add_argument('vm_id', type=int)
parser.add_argument('order_id', type=int)
parser.add_argument('user', type=str)
parser.add_argument('specs', type=str)
parser.add_argument('template', type=str)
def handle(self, *args, **options):
vm_id = options['vm_id']
order_id = options['order_id']
user_str = options['user']
specs_str = options['specs']
template_str = options['template']
json_acceptable_string = user_str.replace("'", "\"")
user_dict = json.loads(json_acceptable_string)
json_acceptable_string = specs_str.replace("'", "\"")
specs = json.loads(json_acceptable_string)
json_acceptable_string = template_str.replace("'", "\"")
template = json.loads(json_acceptable_string)
if vm_id <= 0:
self.stdout.write(self.style.ERROR(
'vm_id can\'t be less than or 0. Given: %s' % vm_id))
return
if vm_id <= 0:
self.stdout.write(self.style.ERROR(
'order_id can\'t be less than or 0. Given: %s' % vm_id))
return
if specs_str is None or specs_str == "":
self.stdout.write(
self.style.ERROR('specs can\'t be empty or None'))
return
user = {
'name': user_dict['name'],
'email': user_dict['email'],
'username': user_dict['username'],
'pass': user_dict['pass'],
'request_scheme': user_dict['request_scheme'],
'request_host': user_dict['request_host'],
'language': user_dict['language'],
}
cu = CustomUser.objects.get(username=user.get('username'))
# Create OpenNebulaManager
self.stdout.write(
self.style.SUCCESS(
'Connecting using %s' % (cu.username)
)
)
manager = OpenNebulaManager(email=cu.username, password=cu.password)
handle_metadata_and_emails(order_id, vm_id, manager, user, specs,
template)
self.stdout.write(
self.style.SUCCESS(
'Done handling metadata and emails for %s %s %s' % (
order_id,
vm_id,
str(user)
)
)
)

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2020-02-04 03:16
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('datacenterlight', '0030_dclnavbarpluginmodel_show_non_transparent_navbar_always'),
]
operations = [
migrations.AddField(
model_name='vmpricing',
name='stripe_coupon_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View file

@ -54,6 +54,7 @@ class VMPricing(models.Model):
discount_amount = models.DecimalField(
max_digits=6, decimal_places=2, default=0
)
stripe_coupon_id = models.CharField(max_length=255, null=True, blank=True)
def __str__(self):
display_str = self.name + ' => ' + ' - '.join([

View file

@ -532,6 +532,7 @@
.order-detail-container .total-price {
font-size: 18px;
line-height: 20px;
}
@media (max-width: 767px) {

View file

@ -225,8 +225,8 @@
}
var total = (cardPricing['cpu'].value * window.coresUnitPrice) +
(cardPricing['ram'].value * window.ramUnitPrice) +
(cardPricing['storage'].value * window.ssdUnitPrice) -
window.discountAmount;
(cardPricing['storage'].value * window.ssdUnitPrice) +
window.vmBasePrice - window.discountAmount;
total = parseFloat(total.toFixed(2));
$("#total").text(total);
}

View file

@ -56,13 +56,8 @@ def create_vm_task(self, vm_template_id, user, specs, template, order_id):
"Running create_vm_task on {}".format(current_task.request.hostname))
vm_id = None
try:
final_price = (
specs.get('total_price') if 'total_price' in specs
else specs.get('price')
)
if 'pass' in user:
on_user = user.get('email')
on_user = user.get('username')
on_pass = user.get('pass')
logger.debug("Using user {user} to create VM".format(user=on_user))
vm_name = None
@ -92,108 +87,8 @@ def create_vm_task(self, vm_template_id, user, specs, template, order_id):
if vm_id is None:
raise Exception("Could not create VM")
# Update HostingOrder with the created vm_id
hosting_order = HostingOrder.objects.filter(id=order_id).first()
error_msg = None
try:
hosting_order.vm_id = vm_id
hosting_order.save()
logger.debug(
"Updated hosting_order {} with vm_id={}".format(
hosting_order.id, vm_id
)
)
except Exception as ex:
error_msg = (
"HostingOrder with id {order_id} not found. This means that "
"the hosting order was not created and/or it is/was not "
"associated with VM with id {vm_id}. Details {details}".format(
order_id=order_id, vm_id=vm_id, details=str(ex)
)
)
logger.error(error_msg)
stripe_utils = StripeUtils()
result = stripe_utils.set_subscription_metadata(
subscription_id=hosting_order.subscription_id,
metadata={"VM_ID": str(vm_id)}
)
if result.get('error') is not None:
emsg = "Could not update subscription metadata for {sub}".format(
sub=hosting_order.subscription_id
)
logger.error(emsg)
if error_msg:
error_msg += ". " + emsg
else:
error_msg = emsg
vm = VirtualMachineSerializer(manager.get_vm(vm_id)).data
context = {
'name': user.get('name'),
'email': user.get('email'),
'cores': specs.get('cpu'),
'memory': specs.get('memory'),
'storage': specs.get('disk_size'),
'price': final_price,
'template': template.get('name'),
'vm_name': vm.get('name'),
'vm_id': vm['vm_id'],
'order_id': order_id
}
if error_msg:
context['errors'] = error_msg
if 'pricing_name' in specs:
context['pricing'] = str(VMPricing.get_vm_pricing_by_name(
name=specs['pricing_name']
))
email_data = {
'subject': settings.DCL_TEXT + " Order from %s" % context['email'],
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': ['info@ungleich.ch'],
'body': "\n".join(
["%s=%s" % (k, v) for (k, v) in context.items()]),
'reply_to': [context['email']],
}
email = EmailMessage(**email_data)
email.send()
if 'pass' in user:
lang = 'en-us'
if user.get('language') is not None:
logger.debug(
"Language is set to {}".format(user.get('language')))
lang = user.get('language')
translation.activate(lang)
# Send notification to the user as soon as VM has been booked
context = {
'base_url': "{0}://{1}".format(user.get('request_scheme'),
user.get('request_host')),
'order_url': reverse('hosting:orders',
kwargs={'pk': order_id}),
'page_header': _(
'Your New VM %(vm_name)s at Data Center Light') % {
'vm_name': vm.get('name')},
'vm_name': vm.get('name')
}
email_data = {
'subject': context.get('page_header'),
'to': user.get('email'),
'context': context,
'template_name': 'new_booked_vm',
'template_path': 'hosting/emails/',
'from_address': settings.DCL_SUPPORT_FROM_ADDRESS,
}
email = BaseEmail(**email_data)
email.send()
logger.debug("New VM ID is {vm_id}".format(vm_id=vm_id))
if vm_id > 0:
get_or_create_vm_detail(custom_user, manager, vm_id)
handle_metadata_and_emails(order_id, vm_id, manager, user, specs,
template)
except Exception as e:
logger.error(str(e))
try:
@ -215,3 +110,127 @@ def create_vm_task(self, vm_template_id, user, specs, template, order_id):
return
return vm_id
def handle_metadata_and_emails(order_id, vm_id, manager, user, specs,
template):
"""
Handle's setting up of the metadata in Stripe and database and sending of
emails to the user after VM creation
:param order_id: the hosting order id
:param vm_id: the id of the vm created
:param manager: the OpenNebula Manager instance
:param user: the user's dict passed to the celery task
:param specs: the specification's dict passed to the celery task
:param template: the template dict passed to the celery task
:return:
"""
custom_user = CustomUser.objects.get(email=user.get('email'))
final_price = (
specs.get('total_price') if 'total_price' in specs
else specs.get('price')
)
# Update HostingOrder with the created vm_id
hosting_order = HostingOrder.objects.filter(id=order_id).first()
error_msg = None
try:
hosting_order.vm_id = vm_id
hosting_order.save()
logger.debug(
"Updated hosting_order {} with vm_id={}".format(
hosting_order.id, vm_id
)
)
except Exception as ex:
error_msg = (
"HostingOrder with id {order_id} not found. This means that "
"the hosting order was not created and/or it is/was not "
"associated with VM with id {vm_id}. Details {details}".format(
order_id=order_id, vm_id=vm_id, details=str(ex)
)
)
logger.error(error_msg)
stripe_utils = StripeUtils()
result = stripe_utils.set_subscription_metadata(
subscription_id=hosting_order.subscription_id,
metadata={"VM_ID": str(vm_id)}
)
if result.get('error') is not None:
emsg = "Could not update subscription metadata for {sub}".format(
sub=hosting_order.subscription_id
)
logger.error(emsg)
if error_msg:
error_msg += ". " + emsg
else:
error_msg = emsg
vm = VirtualMachineSerializer(manager.get_vm(vm_id)).data
context = {
'name': user.get('name'),
'email': user.get('email'),
'cores': specs.get('cpu'),
'memory': specs.get('memory'),
'storage': specs.get('disk_size'),
'price': final_price,
'template': template.get('name'),
'vm_name': vm.get('name'),
'vm_id': vm['vm_id'],
'order_id': order_id
}
if error_msg:
context['errors'] = error_msg
if 'pricing_name' in specs:
context['pricing'] = str(VMPricing.get_vm_pricing_by_name(
name=specs['pricing_name']
))
email_data = {
'subject': settings.DCL_TEXT + " Order from %s" % context['email'],
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': ['dcl-orders@ungleich.ch'],
'body': "\n".join(
["%s=%s" % (k, v) for (k, v) in context.items()]),
'reply_to': [context['email']],
}
email = EmailMessage(**email_data)
email.send()
if 'pass' in user:
lang = 'en-us'
if user.get('language') is not None:
logger.debug(
"Language is set to {}".format(user.get('language')))
lang = user.get('language')
translation.activate(lang)
# Send notification to the user as soon as VM has been booked
context = {
'base_url': "{0}://{1}".format(user.get('request_scheme'),
user.get('request_host')),
'order_url': reverse('hosting:invoices'),
'page_header': _(
'Your New VM %(vm_name)s at Data Center Light') % {
'vm_name': vm.get('name')},
'vm_name': vm.get('name')
}
email_data = {
'subject': context.get('page_header'),
'to': user.get('email'),
'context': context,
'template_name': 'new_booked_vm',
'template_path': 'hosting/emails/',
'from_address': settings.DCL_SUPPORT_FROM_ADDRESS,
}
email = BaseEmail(**email_data)
email.send()
logger.debug("New VM ID is {vm_id}".format(vm_id=vm_id))
if vm_id > 0:
get_or_create_vm_detail(custom_user, manager, vm_id)

View file

@ -1,5 +1,5 @@
<div class="price-calc-section">
<div class="card">
{% include "datacenterlight/includes/_calculator_form.html" with vm_pricing=instance.pricing %}
{% include "datacenterlight/includes/_calculator_form.html" with vm_pricing=instance.pricing vm_base_price=vm_base_price %}
</div>
</div>

View file

@ -28,7 +28,7 @@
{% blocktrans %}Thanks for joining us! We provide the most affordable virtual machines from the heart of Switzerland.{% endblocktrans %}
</p>
<p style="line-height: 1.75; font-family: Lato, Arial, sans-serif; font-weight: 300; margin: 0;">
{% blocktrans %}Try now, order a VM. VM price starts from only 10.5 CHF per month.{% endblocktrans %}
{% blocktrans %}Try now, order a VM. VM price starts from only 11.5 CHF per month.{% endblocktrans %}
</p>
</td>
</tr>

View file

@ -3,7 +3,7 @@
{% trans "Welcome to Data Center Light!" %}
{% blocktrans %}Thanks for joining us! We provide the most affordable virtual machines from the heart of Switzerland.{% endblocktrans %}
{% blocktrans %}Try now, order a VM. VM price starts from only 10.5 CHF per month.{% endblocktrans %}
{% blocktrans %}Try now, order a VM. VM price starts from only 11.5 CHF per month.{% endblocktrans %}
{{ base_url }}{% url 'hosting:create_virtual_machine' %}

View file

@ -9,6 +9,7 @@
window.ssdUnitPrice = {{vm_pricing.ssd_unit_price|default:0}};
window.hddUnitPrice = {{vm_pricing.hdd_unit_price|default:0}};
window.discountAmount = {{vm_pricing.discount_amount|default:0}};
window.vmBasePrice = {{vm_base_price|default:0}};
window.minRam = {{min_ram}};
window.minRamErr = '{% blocktrans with min_ram=min_ram %}Please enter a value in range {{min_ram}} - 200.{% endblocktrans %}';
</script>

View file

@ -13,6 +13,15 @@
<!-- Credit card form -->
<div class="dcl-order-container">
<div class="payment-container">
<div id='payment_error'>
{% for message in messages %}
{% if 'vat_error' in message.tags %}
<ul class="list-unstyled">
<li><p class="card-warning-content card-warning-error">An error occurred while validating VAT number: {{ message|safe }}</p></li>
</ul>
{% endif %}
{% endfor %}
</div>
<div class="dcl-payment-grid">
<div class="dcl-payment-box">
<div class="dcl-payment-section">

View file

@ -2,6 +2,14 @@
{% load staticfiles bootstrap3 i18n custom_tags humanize %}
{% block content %}
<script>
{% if payment_intent_secret %}
console.log("payment_intent_secret");
window.paymentIntentSecret = "{{payment_intent_secret}}";
{% else %}
console.log("No payment_intent_secret");
{% endif %}
</script>
<div id="order-detail{{order.pk}}" class="order-detail-container">
{% if messages %}
<div class="alert alert-warning">
@ -32,6 +40,16 @@
{{billing_address.cardholder_name}}<br>
{{billing_address.street_address}}, {{billing_address.postal_code}}<br>
{{billing_address.city}}, {{billing_address.country}}
{% if billing_address.vat_number %}
<br/>{% trans "VAT Number" %} {{billing_address.vat_number}}
{% if vm.vat_validation_status != "ch_vat" and vm.vat_validation_status != "not_needed" %}
{% if vm.vat_validation_status == "verified" %}
<span class="fa fa-fw fa-check-circle" aria-hidden="true" title='{% trans "Your VAT number has been verified" %}'></span>
{% else %}
<span class="fa fa-fw fa-info-circle" aria-hidden="true" title='{% trans "Your VAT number is under validation. VAT will be adjusted, once the validation is complete." %}'></span>
{% endif %}
{% endif %}
{% endif %}
{% endwith %}
</p>
</address>
@ -48,53 +66,121 @@
<hr>
<div>
<h4>{% trans "Order summary" %}</h4>
<style>
@media screen and (max-width:400px){
.header-no-left-padding {
padding-left: 0 !important;
}
}
@media screen and (max-width:767px){
.cmf-ord-heading {
font-size: 11px;
}
.order-detail-container .order-details {
font-size: 13px;
}
}
@media screen and (max-width:367px){
.cmf-ord-heading {
font-size: 11px;
}
.order-detail-container .order-details {
font-size: 12px;
}
}
</style>
{% if generic_payment_details %}
<div class="row">
<div class="col-sm-9">
<p>
<strong>{% trans "Product" %}:</strong>&nbsp;
{{ generic_payment_details.product_name }}
</p>
<div class="row">
<div class="col-sm-6">
{% if generic_payment_details.vat_rate > 0 %}
<p>
<span>{% trans "Price" %}: </span>
<strong class="pull-right">CHF {{generic_payment_details.amount_before_vat|floatformat:2|intcomma}}</strong>
</p>
<p>
<span>{% trans "VAT for" %} {{generic_payment_details.vat_country}} ({{generic_payment_details.vat_rate}}%) : </span>
<strong class="pull-right">CHF {{generic_payment_details.vat_amount|floatformat:2|intcomma}}</strong>
</p>
<p>
<span>{% trans "Total Amount" %} : </span>
<strong class="pull-right">CHF {{generic_payment_details.amount|floatformat:2|intcomma}}</strong>
</p>
{% else %}
<p>
<span>{% trans "Amount" %}: </span>
<strong class="pull-right">CHF {{generic_payment_details.amount|floatformat:2|intcomma}}</strong>
</p>
{% endif %}
{% if generic_payment_details.description %}
<p>
<span>{% trans "Description" %}: </span>
<strong class="pull-right">{{generic_payment_details.description}}</strong>
</p>
{% endif %}
{% if generic_payment_details.recurring %}
<p>
<span>{% trans "Recurring" %}: </span>
<strong class="pull-right">Yes</strong>
</p>
{% endif %}
</div>
{% if generic_payment_details.description %}
<p>
<strong>{% trans "Description" %}: </strong>
<span class="pull-right">{{generic_payment_details.description}}</span>
</p>
{% endif %}
{% if generic_payment_details.recurring %}
<p>
<strong>{% trans "Recurring" %}: </strong>
<span class="pull-right">Yes</span>
</p>
{% endif %}
</div>
{% if generic_payment_details.exclude_vat_calculations %}
{% else %}
<div class="col-sm-12">
<hr class="thin-hr">
</div>
<div class="col-sm-9">
<p>
<strong class="text-uppercase">{% trans "Price Before VAT" %}</strong>
<strong class="pull-right">{{generic_payment_details.amount_before_vat|floatformat:2|intcomma}} CHF</strong>
</p>
</div>
<div class="col-sm-12">
<hr class="thin-hr">
</div>
<div class="col-sm-9">
<div class="row">
<div class="col-md-4 col-sm-4 col-xs-4">
<p><span></span></p>
</div>
<div class="col-md-3 col-sm-3 col-xs-4">
<p class="text-right"><strong class="cmf-ord-heading">{% trans "Pre VAT" %}</strong></p>
</div>
<div class="col-md-5 col-sm-5 col-xs-4 header-no-left-padding">
<p class="text-right"><strong class="cmf-ord-heading">{% trans "VAT for" %} {{generic_payment_details.vat_country}} ({{generic_payment_details.vat_rate}}%)</strong></p>
</div>
</div>
<div class="row">
<div class="col-md-4 col-sm-4 col-xs-4">
<p><span>Price</span></p>
</div>
<div class="col-md-3 col-sm-3 col-xs-4">
<p><span class="pull-right" >{{generic_payment_details.amount_before_vat|floatformat:2|intcomma}} CHF</span></p>
</div>
<div class="col-md-5 col-sm-5 col-xs-4">
<p><span class="pull-right">{{generic_payment_details.amount|floatformat:2|intcomma}} CHF</span></p>
</div>
</div>
</div>
<div class="col-sm-12">
<hr class="thin-hr">
</div>
<div class="col-sm-9">
<div class="row">
<div class="col-md-4 col-sm-4 col-xs-4">
<p><strong>Total</strong></p>
</div>
<div class="col-md-3 col-sm-3 col-xs-4">
<p><strong class="pull-right">{{generic_payment_details.amount_before_vat|floatformat:2|intcomma}} CHF</strong></p>
</div>
<div class="col-md-5 col-sm-5 col-xs-4">
<p><strong class="pull-right">{{generic_payment_details.amount|floatformat:2|intcomma}} CHF</strong></p>
</div>
</div>
</div>
{% endif %}
<div class="col-sm-12">
<hr class="thin-hr">
</div>
<div class="col-sm-9">
<strong class="text-uppercase align-center">{% trans "Your Price in Total" %}</strong>
<strong class="total-price pull-right">{{generic_payment_details.amount|floatformat:2|intcomma}} CHF</strong>
</div>
</div>
{% else %}
<p>
<strong>{% trans "Product" %}:</strong>&nbsp;
{{ request.session.template.name }}
</p>
<div class="row">
<div class="col-sm-6">
<div class="col-sm-9">
<p>
<span>{% trans "Cores" %}: </span>
<strong class="pull-right">{{vm.cpu|floatformat}}</strong>
@ -111,38 +197,75 @@
<div class="col-sm-12">
<hr class="thin-hr">
</div>
{% if vm.vat > 0 or vm.discount.amount > 0 %}
<div class="col-sm-6">
<div class="subtotal-price">
{% if vm.vat > 0 %}
<p>
<strong class="text-lg">{% trans "Subtotal" %} </strong>
<strong class="pull-right">{{vm.price|floatformat:2|intcomma}} CHF</strong>
</p>
<p>
<small>{% trans "VAT for" %} {{vm.vat_country}} ({{vm.vat_percent}}%) : </small>
<strong class="pull-right">{{vm.vat|floatformat:2|intcomma}} CHF</strong>
</p>
{% endif %}
{% if vm.discount.amount > 0 %}
<p class="text-primary">
{%trans "Discount" as discount_name %}
<strong>{{ vm.discount.name|default:discount_name }} </strong>
<strong class="pull-right">- {{ vm.discount.amount }} CHF</strong>
</p>
{% endif %}
</div>
</div>
<div class="col-sm-12">
<hr class="thin-hr">
</div>
{% endif %}
<div class="col-sm-6">
<p class="total-price">
<strong>{% trans "Total" %} </strong>
<strong class="pull-right">{{vm.total_price|floatformat:2|intcomma}} CHF</strong>
<div class="col-sm-9">
<p>
<strong class="text-uppercase">{% trans "Price Before VAT" %}</strong>
<strong class="pull-right">{{vm.price|floatformat:2|intcomma}} CHF</strong>
</p>
</div>
<div class="col-sm-12">
<hr class="thin-hr">
</div>
<div class="col-sm-9">
<div class="row">
<div class="col-md-4 col-sm-4 col-xs-4">
<p><span></span></p>
</div>
<div class="col-md-3 col-sm-3 col-xs-4">
<p class="text-right"><strong class="cmf-ord-heading">{% trans "Pre VAT" %}</strong></p>
</div>
<div class="col-md-5 col-sm-5 col-xs-4 header-no-left-padding">
<p class="text-right"><strong class="cmf-ord-heading">{% trans "VAT for" %} {{vm.vat_country}} ({{vm.vat_percent}}%)</strong></p>
</div>
</div>
<div class="row">
<div class="col-md-4 col-sm-4 col-xs-4">
<p><span>Price</span></p>
</div>
<div class="col-md-3 col-sm-3 col-xs-4">
<p><span class="pull-right" >{{vm.price|floatformat:2|intcomma}} CHF</span></p>
</div>
<div class="col-md-5 col-sm-5 col-xs-4">
<p><span class="pull-right">{{vm.price_with_vat|floatformat:2|intcomma}} CHF</span></p>
</div>
</div>
{% if vm.discount.amount > 0 %}
<div class="row">
<div class="col-md-4 col-sm-4 col-xs-4">
<p><span>{{vm.discount.name}}</span></p>
</div>
<div class="col-md-3 col-sm-3 col-xs-4">
<p><span class="pull-right">-{{vm.discount.amount|floatformat:2|intcomma}} CHF</span></p>
</div>
<div class="col-md-5 col-sm-5 col-xs-4">
<p><span class="pull-right">-{{vm.discount.amount_with_vat|floatformat:2|intcomma}} CHF</span></p>
</div>
</div>
{% endif %}
</div>
<div class="col-sm-12">
<hr class="thin-hr">
</div>
<div class="col-sm-9">
<div class="row">
<div class="col-md-4 col-sm-4 col-xs-4">
<p><strong>Total</strong></p>
</div>
<div class="col-md-3 col-sm-3 col-xs-4">
<p><strong class="pull-right">{{vm.price_after_discount|floatformat:2|intcomma}} CHF</strong></p>
</div>
<div class="col-md-5 col-sm-5 col-xs-4">
<p><strong class="pull-right">{{vm.price_after_discount_with_vat|floatformat:2|intcomma}} CHF</strong></p>
</div>
</div>
</div>
<div class="col-sm-12">
<hr class="thin-hr">
</div>
<div class="col-sm-9">
<strong class="text-uppercase align-center">{% trans "Your Price in Total" %}</strong>
<strong class="total-price pull-right">{{vm.total_price|floatformat:2|intcomma}} CHF</strong>
</div>
</div>
{% endif %}
</div>
@ -155,15 +278,16 @@
{% if generic_payment_details %}
{% if generic_payment_details.recurring %}
{% if generic_payment_details.recurring_interval == 'year' %}
<div class="dcl-place-order-text">{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{total_price}} CHF/year{% endblocktrans %}.</div>
<div class="dcl-place-order-text">{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %} By clicking "Place order" you agree to our <a href="https://datacenterlight.ch/en-us/cms/terms-of-service/">Terms of Service</a> and this plan will charge your credit card account with {{ total_price }} CHF/year{% endblocktrans %}.</div>
{% else %}
<div class="dcl-place-order-text">{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{total_price}} CHF/month{% endblocktrans %}.</div>
<div class="dcl-place-order-text">{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}
By clicking "Place order" you agree to our <a href="https://datacenterlight.ch/en-us/cms/terms-of-service/">Terms of Service</a> and this plan will charge your credit card account with {{ total_price }} CHF/month{% endblocktrans %}.</div>
{% endif %}
{% else %}
<div class="dcl-place-order-text">{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}By clicking "Place order" this payment will charge your credit card account with a one time amount of {{total_price}} CHF{% endblocktrans %}.</div>
<div class="dcl-place-order-text">{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}By clicking "Place order" you agree to our <a href="https://datacenterlight.ch/en-us/cms/terms-of-service/">Terms of Service</a> and this plan will charge your credit card account with {{ total_price }} CHF{% endblocktrans %}.</div>
{% endif %}
{% else %}
<div class="dcl-place-order-text">{% blocktrans with vm_total_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{vm_total_price}} CHF/month{% endblocktrans %}.</div>
<div class="dcl-place-order-text">{% blocktrans with vm_total_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" you agree to our <a href="https://datacenterlight.ch/en-us/cms/terms-of-service/">Terms of Service</a> and this plan will charge your credit card account with {{ vm_total_price }} CHF/month{% endblocktrans %}.</div>
{% endif %}
</div>
<div class="col-sm-4 order-confirm-btn text-right">
@ -206,5 +330,14 @@
<script type="text/javascript">
{% trans "Some problem encountered. Please try again later." as err_msg %}
var create_vm_error_message = '{{err_msg|safe}}';
var pm_id = '{{id_payment_method}}';
var error_url = '{{ error_msg.redirect }}';
var error_msg = '{{ error_msg.msg_body }}';
var error_title = '{{ error_msg.msg_title }}';
var success_msg = '{{ success_msg.msg_body }}';
var success_title = '{{ success_msg.msg_title }}';
var success_url = '{{ success_msg.redirect }}';
window.stripeKey = "{{stripe_key}}";
window.isSubscription = ("{{is_subscription}}" === 'true');
</script>
{%endblock%}
{%endblock%}

View file

@ -1,6 +1,15 @@
import datetime
import logging
from django import template
from django.core.urlresolvers import resolve, reverse
from django.utils.translation import activate, get_language
from django.utils.safestring import mark_safe
from django.utils.translation import activate, get_language, ugettext_lazy as _
from hosting.models import GenericProduct, HostingOrder
from utils.hosting_utils import get_ip_addresses
logger = logging.getLogger(__name__)
register = template.Library()
@ -52,3 +61,116 @@ def escaped_line_break(value):
:return:
"""
return value.replace("\\n", "\n")
@register.filter('get_line_item_from_hosting_order_charge')
def get_line_item_from_hosting_order_charge(hosting_order_id):
"""
Returns ready-to-use "html" line item to be shown for a charge in the
invoice list page
:param hosting_order_id: the HostingOrder id
:return:
"""
try:
hosting_order = HostingOrder.objects.get(id = hosting_order_id)
if hosting_order.stripe_charge_id:
return mark_safe("""
<td class="xs-td-inline">{product_name}</td>
<td class="xs-td-inline">{created_at}</td>
<td class="xs-td-inline">{total}</td>
<td class="text-right last-td">
<a class="btn btn-order-detail" href="{receipt_url}" target="_blank">{see_invoice_text}</a>
</td>
""".format(
product_name=hosting_order.generic_product.product_name.capitalize(),
created_at=hosting_order.created_at.strftime('%Y-%m-%d'),
total='%.2f' % (hosting_order.price),
receipt_url=reverse('hosting:orders',
kwargs={'pk': hosting_order.id}),
see_invoice_text=_("See Invoice")
))
else:
return ""
except Exception as ex:
logger.error("Error %s" % str(ex))
return ""
@register.filter('get_line_item_from_stripe_invoice')
def get_line_item_from_stripe_invoice(invoice):
"""
Returns ready-to-use "html" line item to be shown for an invoice in the
invoice list page
:param invoice: the stripe Invoice object
:return:
"""
start_date = 0
end_date = 0
is_first = True
vm_id = -1
plan_name = ""
for line_data in invoice["lines"]["data"]:
if is_first:
plan_name = line_data.plan.name if line_data.plan is not None else ""
start_date = line_data.period.start
end_date = line_data.period.end
is_first = False
if hasattr(line_data.metadata, "VM_ID"):
vm_id = line_data.metadata.VM_ID
else:
if line_data.period.start < start_date:
start_date = line_data.period.start
if line_data.period.end > end_date:
end_date = line_data.period.end
if hasattr(line_data.metadata, "VM_ID"):
vm_id = line_data.metadata.VM_ID
try:
vm_id = int(vm_id)
except ValueError as ve:
print(str(ve))
if invoice["lines"]["data"]:
return mark_safe("""
<td class="xs-td-inline">{vm_id}</td>
<td class="xs-td-inline">{ip_addresses}</td>
<td class="xs-td-inline">{period}</td>
<td class="xs-td-inline text-right dcl-text-right-padding">{total}</td>
<td class="text-right last-td">
<a class="btn btn-order-detail" href="{stripe_invoice_url}" target="_blank">{see_invoice_text}</a>
</td>
""".format(
vm_id=vm_id if vm_id > 0 else "",
ip_addresses=mark_safe(get_ip_addresses(vm_id)) if vm_id > 0 else
mark_safe(get_product_name(plan_name)),
period=mark_safe("%s &mdash; %s" % (
datetime.datetime.fromtimestamp(start_date).strftime('%Y-%m-%d'),
datetime.datetime.fromtimestamp(end_date).strftime('%Y-%m-%d'))),
total='%.2f' % (invoice.total/100),
stripe_invoice_url=invoice.hosted_invoice_url,
see_invoice_text=_("See Invoice")
))
else:
return ""
def get_product_name(plan_name):
product_name = ""
if plan_name and plan_name.startswith("generic-"):
first_index_hyphen = plan_name.index("-") + 1
product_id = plan_name[first_index_hyphen:
(plan_name[first_index_hyphen:].index("-")) + first_index_hyphen]
try:
product = GenericProduct.objects.get(id=product_id)
product_name = product.product_name
except GenericProduct.DoesNotExist as dne:
logger.error("Generic product id=%s does not exist" % product_id)
product_name = plan_name
except GenericProduct.MultipleObjectsReturned as mor:
logger.error("Multiple products with id=%s exist" % product_id)
product_name = "Unknown"
else:
logger.debug("Product name for plan %s does not exist" % plan_name)
return product_name

View file

@ -1,7 +1,9 @@
import datetime
import logging
import pyotp
import requests
import stripe
from django.conf import settings
from django.contrib.sites.models import Site
@ -9,12 +11,19 @@ from datacenterlight.tasks import create_vm_task
from hosting.models import HostingOrder, HostingBill, OrderDetail
from membership.models import StripeCustomer
from utils.forms import UserBillingAddressForm
from utils.models import BillingAddress
from utils.models import BillingAddress, UserBillingAddress
from utils.stripe_utils import StripeUtils
from .cms_models import CMSIntegration
from .models import VMPricing, VMTemplate
logger = logging.getLogger(__name__)
eu_countries = ['at', 'be', 'bg', 'ch', 'cy', 'cz', 'hr', 'dk',
'ee', 'fi', 'fr', 'mc', 'de', 'gr', 'hu', 'ie', 'it',
'lv', 'lu', 'mt', 'nl', 'pl', 'pt', 'ro','sk', 'si', 'es',
'se', 'gb']
def get_cms_integration(name):
current_site = Site.objects.get_current()
try:
@ -29,12 +38,14 @@ def get_cms_integration(name):
def create_vm(billing_address_data, stripe_customer_id, specs,
stripe_subscription_obj, card_details_dict, request,
vm_template_id, template, user):
logger.debug("In create_vm")
billing_address = BillingAddress(
cardholder_name=billing_address_data['cardholder_name'],
street_address=billing_address_data['street_address'],
city=billing_address_data['city'],
postal_code=billing_address_data['postal_code'],
country=billing_address_data['country']
country=billing_address_data['country'],
vat_number=billing_address_data['vat_number'],
)
billing_address.save()
customer = StripeCustomer.objects.filter(id=stripe_customer_id).first()
@ -92,8 +103,6 @@ def create_vm(billing_address_data, stripe_customer_id, specs,
create_vm_task.delay(vm_template_id, user, specs, template, order.id)
clear_all_session_vars(request)
def clear_all_session_vars(request):
if request.session is not None:
@ -101,7 +110,9 @@ def clear_all_session_vars(request):
'billing_address_data', 'card_id',
'token', 'customer', 'generic_payment_type',
'generic_payment_details', 'product_id',
'order_confirm_url', 'new_user_hosting_key_id']:
'order_confirm_url', 'new_user_hosting_key_id',
'vat_validation_status', 'billing_address_id',
'id_payment_method']:
if session_var in request.session:
del request.session[session_var]
@ -123,3 +134,172 @@ def check_otp(name, realm, token):
data=data
)
return response.status_code
def validate_vat_number(stripe_customer_id, billing_address_id,
is_user_ba=False):
if is_user_ba:
try:
billing_address = UserBillingAddress.objects.get(
id=billing_address_id)
except UserBillingAddress.DoesNotExist as dne:
billing_address = None
logger.debug(
"UserBillingAddress does not exist for %s" % billing_address_id)
except UserBillingAddress.MultipleObjectsReturned as mor:
logger.debug(
"Multiple UserBillingAddress exist for %s" % billing_address_id)
billing_address = UserBillingAddress.objects.filter(
id=billing_address_id).order_by('-id').first()
else:
try:
billing_address = BillingAddress.objects.get(id=billing_address_id)
except BillingAddress.DoesNotExist as dne:
billing_address = None
logger.debug("BillingAddress does not exist for %s" % billing_address_id)
except BillingAddress.MultipleObjectsReturned as mor:
logger.debug("Multiple BillingAddress exist for %s" % billing_address_id)
billing_address = BillingAddress.objects.filter(id=billing_address_id).order_by('-id').first()
if billing_address is not None:
logger.debug("BillingAddress found: %s %s type=%s" % (
billing_address_id, str(billing_address), type(billing_address)))
if billing_address.country.lower().strip() not in eu_countries:
return {
"validated_on": "",
"status": "not_needed"
}
if billing_address.vat_number_validated_on:
logger.debug("billing_address verified on %s" %
billing_address.vat_number_validated_on)
return {
"validated_on": billing_address.vat_number_validated_on,
"status": "verified"
}
else:
logger.debug("billing_address not yet verified, "
"Checking if we already have a tax id")
if billing_address.stripe_tax_id:
logger.debug("We have a tax id %s" % billing_address.stripe_tax_id)
tax_id_obj = stripe.Customer.retrieve_tax_id(
stripe_customer_id,
billing_address.stripe_tax_id,
)
if tax_id_obj.verification.status == "verified":
logger.debug("Latest status on Stripe=%s. Updating" %
tax_id_obj.verification.status)
# update billing address
billing_address.vat_number_validated_on = datetime.datetime.now()
billing_address.vat_validation_status = tax_id_obj.verification.status
billing_address.save()
return {
"status": "verified",
"validated_on": billing_address.vat_number_validated_on
}
else:
billing_address.vat_validation_status = tax_id_obj.verification.status
billing_address.save()
logger.debug(
"Latest status on Stripe=%s" % str(tax_id_obj)
)
return {
"status": tax_id_obj.verification.status if tax_id_obj
else "unknown",
"validated_on": ""
}
else:
logger.debug("Creating a tax id")
logger.debug("Billing address = %s" % str(billing_address))
tax_id_obj = create_tax_id(
stripe_customer_id, billing_address_id,
"ch_vat" if billing_address.country.lower() == "ch" else "eu_vat",
is_user_ba=is_user_ba
)
logger.debug("tax_id_obj = %s" % str(tax_id_obj))
else:
logger.debug("invalid billing address")
return {
"status": "invalid billing address",
"validated_on": ""
}
if 'response_object' in tax_id_obj:
return tax_id_obj
return {
"status": tax_id_obj.verification.status,
"validated_on": datetime.datetime.now() if tax_id_obj.verification.status == "verified" else ""
}
def create_tax_id(stripe_customer_id, billing_address_id, type,
is_user_ba=False):
if is_user_ba:
try:
billing_address = UserBillingAddress.objects.get(
id=billing_address_id)
except UserBillingAddress.DoesNotExist as dne:
billing_address = None
logger.debug(
"UserBillingAddress does not exist for %s" % billing_address_id)
except UserBillingAddress.MultipleObjectsReturned as mor:
logger.debug(
"Multiple UserBillingAddress exist for %s" % billing_address_id)
billing_address = UserBillingAddress.objects.filter(
id=billing_address_id).order_by('-id').first()
else:
try:
billing_address = BillingAddress.objects.get(id=billing_address_id)
except BillingAddress.DoesNotExist as dne:
billing_address = None
logger.debug("BillingAddress does not exist for %s" % billing_address_id)
except BillingAddress.MultipleObjectsReturned as mor:
logger.debug("Multiple BillingAddress exist for %s" % billing_address_id)
billing_address = BillingAddress.objects.filter(billing_address_id).order_by('-id').first()
tax_id_obj = None
if billing_address:
stripe_utils = StripeUtils()
tax_id_response = stripe_utils.get_or_create_tax_id_for_user(
stripe_customer_id,
vat_number=billing_address.vat_number,
type=type,
country=billing_address.country
)
tax_id_obj = tax_id_response.get('response_object')
if not tax_id_obj:
logger.debug("Received none in tax_id_obj")
return {
'paid': False,
'response_object': None,
'error': "No such address found" if 'error' not in tax_id_response else
tax_id_response["error"]
}
try:
stripe_customer = StripeCustomer.objects.get(stripe_id=stripe_customer_id)
billing_address_set = set()
logger.debug("Updating billing address")
for ho in stripe_customer.hostingorder_set.all():
if ho.billing_address.vat_number == billing_address.vat_number:
billing_address_set.add(ho.billing_address)
for b_address in billing_address_set:
b_address.stripe_tax_id = tax_id_obj.id
b_address.vat_validation_status = tax_id_obj.verification.status
b_address.save()
logger.debug("Updated billing_address %s" % str(b_address))
ub_addresses = stripe_customer.user.billing_addresses.filter(
vat_number=billing_address.vat_number)
for ub_address in ub_addresses:
ub_address.stripe_tax_id = tax_id_obj.id
ub_address.vat_validation_status = tax_id_obj.verification.status
ub_address.save()
logger.debug("Updated user_billing_address %s" % str(ub_address))
except StripeCustomer.DoesNotExist as dne:
logger.debug("StripeCustomer %s does not exist" % stripe_customer_id)
billing_address.stripe_tax_id = tax_id_obj.id
billing_address.vat_validation_status = tax_id_obj.verification.status
billing_address.save()
return tax_id_obj

File diff suppressed because it is too large Load diff

View file

@ -35,6 +35,7 @@ class MembershipBillingForm(BillingAddressForm):
'city': _('City'),
'postal_code': _('Postal Code'),
'country': _('Country'),
'vat_number': _('VAT Number'),
}

View file

@ -376,8 +376,6 @@ msgid ""
" digitalglarus.ch<br/>\n"
" hack4lgarus.ch<br/>\n"
" ipv6onlyhosting.com<br/>\n"
" ipv6onlyhosting.ch<br/>\n"
" ipv6onlyhosting.net<br/>\n"
" django-hosting.ch<br/>\n"
" rails-hosting.ch<br/>\n"
" node-hosting.ch<br/>\n"
@ -636,8 +634,8 @@ msgstr ""
"Internetangebot der ungleich glarus ag, welches unter den nachfolgenden "
"Domains erreichbar ist:<br/><br/>ungleich.ch<br/>datacenterlight.ch<br/"
">devuanhosting.com<br/>devuanhosting.ch<br/>digitalglarus.ch<br/>hack4lgarus."
"ch<br/>ipv6onlyhosting.com<br/>ipv6onlyhosting.ch<br/>ipv6onlyhosting.net<br/"
">django-hosting.ch<br/>rails-hosting.ch<br/>node-hosting.ch<br/>blog."
"ch<br/>ipv6onlyhosting.com<br/>django-hosting.ch<br/>rails-hosting.ch"
"<br/>node-hosting.ch<br/>blog."
"ungleich.ch<br/><br/>Der Datenschutzbeauftragte des Verantwortlichen ist:<br/"
"><br/>Sanghee Kim<br/>ungleich glarus ag<br/>Bahnhofstrasse 1<br/>8783 "
"Linthal (CH)<br/>E-Mail: <a href=\"mailto:sanghee.kim@ungleich.ch\">sanghee."
@ -838,3 +836,4 @@ msgstr ""
#~ msgid "index/?$"
#~ msgstr "index/?$"

View file

@ -153,6 +153,7 @@ INSTALLED_APPS = (
'rest_framework',
'opennebula_api',
'django_celery_results',
'webhook',
)
MIDDLEWARE_CLASSES = (
@ -630,8 +631,6 @@ GOOGLE_ANALYTICS_PROPERTY_IDS = {
'datacenterlight.ch': 'UA-62285904-8',
'devuanhosting.ch': 'UA-62285904-9',
'devuanhosting.com': 'UA-62285904-9',
'ipv6onlyhosting.ch': 'UA-62285904-10',
'ipv6onlyhosting.net': 'UA-62285904-10',
'ipv6onlyhosting.com': 'UA-62285904-10',
'comic.ungleich.ch': 'UA-62285904-13',
'127.0.0.1:8000': 'localhost',
@ -704,7 +703,7 @@ if ENABLE_LOGGING:
'disable_existing_loggers': False,
'formatters': {
'standard': {
'format': '%(asctime)s %(levelname)s %(name)s: %(message)s'
'format': '%(asctime)s,%(msecs)d %(levelname)-8s [%(filename)s:%(lineno)d] %(message)s',
}
},
'handlers': handlers_dict,
@ -720,7 +719,10 @@ X_FRAME_OPTIONS = ('SAMEORIGIN' if X_FRAME_OPTIONS_ALLOW_FROM_URI is None else
X_FRAME_OPTIONS_ALLOW_FROM_URI.strip()
))
WEBHOOK_SECRET = env('WEBHOOK_SECRET')
DEBUG = bool_env('DEBUG')
ADD_TRIAL_PERIOD_TO_SUBSCRIPTION = bool_env('ADD_TRIAL_PERIOD_TO_SUBSCRIPTION')
# LDAP setup
@ -757,6 +759,15 @@ OTP_VERIFY_ENDPOINT = env('OTP_VERIFY_ENDPOINT')
FIRST_VM_ID_AFTER_EU_VAT = int_env('FIRST_VM_ID_AFTER_EU_VAT')
PRE_EU_VAT_RATE = float(env('PRE_EU_VAT_RATE'))
VM_BASE_PRICE = float(env('VM_BASE_PRICE'))
UPDATED_TEMPLATES_STR = env('UPDATED_TEMPLATES')
UPDATED_TEMPLATES_DICT = {}
if UPDATED_TEMPLATES_STR:
UPDATED_TEMPLATES_DICT = eval(UPDATED_TEMPLATES_STR)
MAX_TIME_TO_WAIT_FOR_VM_TERMINATE = int_env(
'MAX_TIME_TO_WAIT_FOR_VM_TERMINATE', 15)
if DEBUG:
from .local import * # flake8: noqa

View file

@ -28,9 +28,7 @@ ALLOWED_HOSTS = [
".devuanhosting.ch",
".devuanhosting.com",
".digitalezukunft.ch",
".ipv6onlyhosting.ch",
".ipv6onlyhosting.com",
".ipv6onlyhosting.net",
".digitalglarus.ch",
".hack4glarus.ch",
".xn--nglarus-n2a.ch"

View file

@ -11,6 +11,7 @@ from hosting.views import (
RailsHostingView, DjangoHostingView, NodeJSHostingView
)
from datacenterlight.views import PaymentOrderView
from webhook import views as webhook_views
from membership import urls as membership_urls
from ungleich_page.views import LandingView
from django.views.generic import RedirectView
@ -62,6 +63,7 @@ urlpatterns += i18n_patterns(
name='blog_list_view'),
url(r'^cms/', include('cms.urls')),
url(r'^blog/', include('djangocms_blog.urls', namespace='djangocms_blog')),
url(r'^webhooks/', webhook_views.handle_webhook),
url(r'^$', RedirectView.as_view(url='/cms') if REDIRECT_TO_CMS
else LandingView.as_view()),
url(r'^', include('ungleich_page.urls', namespace='ungleich_page')),

View file

@ -2,6 +2,7 @@ import datetime
import logging
import subprocess
import tempfile
import xml
from django import forms
from django.conf import settings
@ -207,7 +208,7 @@ class UserHostingKeyForm(forms.ModelForm):
logger.debug(
"Not a correct ssh format {error}".format(error=str(cpe)))
raise forms.ValidationError(KEY_ERROR_MESSAGE)
return openssh_pubkey_str
return xml.sax.saxutils.escape(openssh_pubkey_str)
def clean_name(self):
INVALID_NAME_MESSAGE = _("Comma not accepted in the name of the key")

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-11-15 16:40+0000\n"
"POT-Creation-Date: 2021-02-07 10:19+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -211,6 +211,9 @@ msgstr "Bezahlbares VM Hosting in der Schweiz"
msgid "My Dashboard"
msgstr "Mein Dashboard"
msgid "Welcome"
msgstr ""
msgid "My VMs"
msgstr "Meine VMs"
@ -364,6 +367,11 @@ msgstr "Abgelehnt"
msgid "Billed to"
msgstr "Rechnungsadresse"
#, fuzzy
#| msgid "Card Number"
msgid "VAT Number"
msgstr "Kreditkartennummer"
msgid "Payment method"
msgstr "Bezahlmethode"
@ -391,6 +399,9 @@ msgstr "Festplattenkapazität"
msgid "Subtotal"
msgstr "Zwischensumme"
msgid "VAT for"
msgstr ""
msgid "VAT"
msgstr "Mehrwertsteuer"
@ -424,18 +435,22 @@ msgstr "ZURÜCK ZUR LISTE"
msgid "Some problem encountered. Please try again later."
msgstr "Ein Problem ist aufgetreten. Bitte versuche es später noch einmal."
#, fuzzy
#| msgid "Description"
msgid "Subscriptions"
msgstr "Beschreibung"
#, fuzzy
#| msgid "One time payment"
msgid "One-time payments"
msgstr "Einmalzahlung"
msgid "VM ID"
msgstr ""
msgid "IP Address"
msgstr "IP-Adresse"
msgid "See Invoice"
msgstr "Siehe Rechnung"
msgid "Page"
msgstr "Seite"
msgid "Log in"
msgstr "Anmelden"
@ -480,11 +495,13 @@ msgstr "Bestellungsübersicht"
#, python-format
msgid ""
"By clicking \"Place order\" this plan will charge your credit card account "
"with %(vm_price)s CHF/month"
"By clicking \"Place order\" you agree to our <a href=\"https://"
"datacenterlight.ch/en-us/cms/terms-of-service/\">Terms of Service</a> and "
"this plan will charge your credit card account with %(vm_price)s CHF/month."
msgstr ""
"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit %(vm_price)s CHF "
"pro Monat belastet"
"Indem Du auf \"Bestellung aufgeben\" klickst, erklärst Du dich mit unseren"
" <a href=\"https://"
"datacenterlight.ch/de/cms/terms-of-service/\">Nutzungsbedingungen</a> einverstanden und Dein Kreditkartenkonto wird mit %(vm_price)s CHF/Monat belastet."
msgid "Place order"
msgstr "Bestellen"
@ -504,6 +521,12 @@ msgstr "Schliessen"
msgid "Order Nr."
msgstr "Bestellung Nr."
msgid "See Invoice"
msgstr "Siehe Rechnung"
msgid "Page"
msgstr "Seite"
msgid "Your Order"
msgstr "Deine Bestellung"
@ -572,6 +595,19 @@ msgstr "Absenden"
msgid "Password reset"
msgstr "Passwort zurücksetzen"
#, fuzzy
#| msgid "Key name"
msgid "My Username"
msgstr "Key-Name"
msgid "Your VAT number has been verified"
msgstr ""
msgid ""
"Your VAT number is under validation. VAT will be adjusted, once the "
"validation is complete."
msgstr ""
msgid "UPDATE"
msgstr "AKTUALISIEREN"
@ -773,21 +809,15 @@ msgstr "Dein Passwort konnte nicht zurückgesetzt werden."
msgid "The reset password link is no longer valid."
msgstr "Der Link zum Zurücksetzen Deines Passwortes ist nicht mehr gültig."
msgid "Could not set a default card."
msgstr ""
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 ""
@ -852,7 +882,8 @@ msgstr "Ungültige Speicher-Grösse"
#, python-brace-format
msgid "Incorrect pricing name. Please contact support{support_email}"
msgstr "Ungültige Preisbezeichnung. Bitte kontaktiere den Support{support_email}"
msgstr ""
"Ungültige Preisbezeichnung. Bitte kontaktiere den Support{support_email}"
msgid ""
"We could not find the requested VM. Please "
@ -871,7 +902,9 @@ msgstr "Fehler beenden VM"
msgid ""
"VM terminate action timed out. Please contact support@datacenterlight.ch for "
"further information."
msgstr "VM beendet wegen Zeitüberschreitung. Bitte kontaktiere support@datacenterlight.ch für weitere Informationen."
msgstr ""
"VM beendet wegen Zeitüberschreitung. Bitte kontaktiere "
"support@datacenterlight.ch für weitere Informationen."
#, python-format
msgid "Virtual Machine %(vm_name)s Cancelled"
@ -882,6 +915,15 @@ msgstr ""
"Es gab einen Fehler bei der Bearbeitung Deine Anfrage. Bitte versuche es "
"noch einmal."
#~ 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 "You seem to have already added this card"
#~ msgstr "Es scheint, als hättest du diese Karte bereits hinzugefügt"
#, python-format
#~ msgid "This key exists already with the name \"%(name)s\""
#~ msgstr "Der SSH-Key mit dem Name \"%(name)s\" existiert bereits"

View file

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2020-01-05 04:29
from __future__ import unicode_literals
from django.db import migrations, models
import utils.mixins
class Migration(migrations.Migration):
dependencies = [
('hosting', '0058_genericproduct_product_subscription_interval'),
]
operations = [
migrations.CreateModel(
name='StripeTaxRate',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tax_rate_id', models.CharField(max_length=100, unique=True)),
('jurisdiction', models.CharField(max_length=10)),
('inclusive', models.BooleanField(default=False)),
('display_name', models.CharField(max_length=100)),
('percentage', models.FloatField(default=0)),
('description', models.CharField(max_length=100)),
],
bases=(utils.mixins.AssignPermissionsMixin, models.Model),
),
]

View file

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2020-06-30 19:12
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('hosting', '0059_stripetaxrate'),
]
operations = [
migrations.RunSQL(
sql=["update hosting_vatrates set stop_date = '2020-06-30' where territory_codes = 'DE' and rate = '0.19'"],
reverse_sql=[
"update hosting_vatrates set stop_date = null where stop_date = '2020-06-30' and territory_codes = 'DE' and rate = '0.19'"],
),
migrations.RunSQL(
sql=[
"insert into hosting_vatrates (start_date, stop_date, territory_codes, currency_code, rate, rate_type, description) values ('2020-07-01',null,'DE', 'EUR', '0.16', 'standard', 'Germany (member state) standard VAT rate - COVID 19 reduced rate')"],
reverse_sql=[
"delete from hosting_vatrates where description = 'Germany (member state) standard VAT rate - COVID 19 reduced rate' and start_date = '2020-07-01' and territory_codes = 'DE'" ],
),
migrations.RunSQL(
sql=[
"update hosting_stripetaxrate set description = 'VAT for DE pre-COVID-19' where description = 'VAT for DE'"],
reverse_sql=[
"update hosting_stripetaxrate set description = 'VAT for DE' where description = 'VAT for DE pre-COVID-19'"],
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2020-07-21 16:32
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hosting', '0060_update_DE_vat_covid-19'),
]
operations = [
migrations.AddField(
model_name='genericproduct',
name='exclude_vat_calculations',
field=models.BooleanField(default=False, help_text='When checked VAT calculations are excluded for this product'),
),
]

View file

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2020-12-23 05:36
from __future__ import unicode_literals
from django.db import migrations, models
import utils.mixins
class Migration(migrations.Migration):
dependencies = [
('hosting', '0061_genericproduct_exclude_vat_calculations'),
]
operations = [
migrations.CreateModel(
name='IncompleteSubscriptions',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('completed_at', models.DateTimeField()),
('subscription_id', models.CharField(max_length=100)),
('subscription_status', models.CharField(max_length=30)),
('name', models.CharField(max_length=50)),
('email', models.EmailField(max_length=254)),
('request', models.TextField()),
('stripe_api_cus_id', models.CharField(max_length=30)),
('card_details_response', models.TextField()),
('stripe_subscription_obj', models.TextField()),
('stripe_onetime_charge', models.TextField()),
('gp_details', models.TextField()),
('specs', models.TextField()),
('vm_template_id', models.PositiveIntegerField(default=0)),
('template', models.TextField()),
('billing_address_data', models.TextField()),
],
bases=(utils.mixins.AssignPermissionsMixin, models.Model),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2020-12-23 06:12
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hosting', '0062_incompletesubscriptions'),
]
operations = [
migrations.AlterField(
model_name='incompletesubscriptions',
name='completed_at',
field=models.DateTimeField(null=True),
),
]

View file

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2020-12-31 10:13
from __future__ import unicode_literals
from django.db import migrations, models
import utils.mixins
class Migration(migrations.Migration):
dependencies = [
('hosting', '0063_auto_20201223_0612'),
]
operations = [
migrations.CreateModel(
name='IncompletePaymentIntents',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('completed_at', models.DateTimeField(null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('payment_intent_id', models.CharField(max_length=100)),
('request', models.TextField()),
('stripe_api_cus_id', models.CharField(max_length=30)),
('card_details_response', models.TextField()),
('stripe_subscription_id', models.TextField()),
('stripe_charge_id', models.TextField()),
('gp_details', models.TextField()),
('billing_address_data', models.TextField()),
],
bases=(utils.mixins.AssignPermissionsMixin, models.Model),
),
]

View file

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2020-12-31 10:41
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hosting', '0064_incompletepaymentintents'),
]
operations = [
migrations.AlterField(
model_name='incompletepaymentintents',
name='stripe_charge_id',
field=models.CharField(max_length=100, null=True),
),
migrations.AlterField(
model_name='incompletepaymentintents',
name='stripe_subscription_id',
field=models.CharField(max_length=100, null=True),
),
]

View file

@ -1,4 +1,3 @@
import decimal
import json
import logging
import os
@ -82,15 +81,22 @@ class GenericProduct(AssignPermissionsMixin, models.Model):
product_subscription_interval = models.CharField(
max_length=10, default="month",
help_text="Choose between `year` and `month`")
exclude_vat_calculations = models.BooleanField(
default=False,
help_text="When checked VAT calculations are excluded for this product"
)
def __str__(self):
return self.product_name
def get_actual_price(self, vat_rate=None):
VAT = vat_rate if vat_rate is not None else self.product_vat
return round(
float(self.product_price) + float(self.product_price) * float(VAT), 2
)
if self.exclude_vat_calculations:
return round(float(self.product_price), 2)
else:
VAT = vat_rate if vat_rate is not None else self.product_vat
return round(
float(self.product_price) + float(self.product_price) * float(VAT), 2
)
class HostingOrder(AssignPermissionsMixin, models.Model):
@ -163,8 +169,12 @@ class HostingOrder(AssignPermissionsMixin, models.Model):
def set_stripe_charge(self, stripe_charge):
self.stripe_charge_id = stripe_charge.id
self.last4 = stripe_charge.source.last4
self.cc_brand = stripe_charge.source.brand
if stripe_charge.source is None:
self.last4 = stripe_charge.payment_method_details.card.last4
self.cc_brand = stripe_charge.payment_method_details.card.brand
else:
self.last4 = stripe_charge.source.last4
self.cc_brand = stripe_charge.source.brand
self.save()
def set_subscription_id(self, subscription_id, cc_details):
@ -666,7 +676,11 @@ class UserCardDetail(AssignPermissionsMixin, models.Model):
stripe_utils = StripeUtils()
cus_response = stripe_utils.get_customer(stripe_api_cus_id)
cu = cus_response['response_object']
cu.default_source = stripe_source_id
if stripe_source_id.startswith("pm"):
# card is a payment method
cu.invoice_settings.default_payment_method = stripe_source_id
else:
cu.default_source = stripe_source_id
cu.save()
UserCardDetail.save_default_card_local(
stripe_api_cus_id, stripe_source_id
@ -724,4 +738,45 @@ class VATRates(AssignPermissionsMixin, models.Model):
currency_code = models.CharField(max_length=10)
rate = models.FloatField()
rate_type = models.TextField(blank=True, default='')
description = models.TextField(blank=True, default='')
description = models.TextField(blank=True, default='')
class StripeTaxRate(AssignPermissionsMixin, models.Model):
tax_rate_id = models.CharField(max_length=100, unique=True)
jurisdiction = models.CharField(max_length=10)
inclusive = models.BooleanField(default=False)
display_name = models.CharField(max_length=100)
percentage = models.FloatField(default=0)
description = models.CharField(max_length=100)
class IncompletePaymentIntents(AssignPermissionsMixin, models.Model):
completed_at = models.DateTimeField(null=True)
created_at = models.DateTimeField(auto_now_add=True)
payment_intent_id = models.CharField(max_length=100)
request = models.TextField()
stripe_api_cus_id = models.CharField(max_length=30)
card_details_response = models.TextField()
stripe_subscription_id = models.CharField(max_length=100, null=True)
stripe_charge_id = models.CharField(max_length=100, null=True)
gp_details = models.TextField()
billing_address_data = models.TextField()
class IncompleteSubscriptions(AssignPermissionsMixin, models.Model):
created_at = models.DateTimeField(auto_now_add=True)
completed_at = models.DateTimeField(null=True)
subscription_id = models.CharField(max_length=100)
subscription_status = models.CharField(max_length=30)
name = models.CharField(max_length=50)
email = models.EmailField()
request = models.TextField()
stripe_api_cus_id = models.CharField(max_length=30)
card_details_response = models.TextField()
stripe_subscription_obj = models.TextField()
stripe_onetime_charge = models.TextField()
gp_details = models.TextField()
specs = models.TextField()
vm_template_id = models.PositiveIntegerField(default=0)
template = models.TextField()
billing_address_data = models.TextField()

View file

@ -2,4 +2,10 @@
.orders-container .table > tbody > tr > td {
vertical-align: middle;
}
@media screen and (min-width:767px){
.dcl-text-right {
padding-right: 20px;
}
}

View file

@ -266,8 +266,8 @@ $( document ).ready(function() {
}
var total = (cardPricing['cpu'].value * window.coresUnitPrice) +
(cardPricing['ram'].value * window.ramUnitPrice) +
(cardPricing['storage'].value * window.ssdUnitPrice) -
window.discountAmount;
(cardPricing['storage'].value * window.ssdUnitPrice) +
window.vmBasePrice - window.discountAmount;
total = parseFloat(total.toFixed(2));
$("#total").text(total);
}

View file

@ -84,68 +84,72 @@ $(document).ready(function () {
var hasCreditcard = window.hasCreditcard || false;
if (!hasCreditcard && window.stripeKey) {
var stripe = Stripe(window.stripeKey);
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'
if (window.pm_id) {
} else {
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")'
}
},
invalid: {
iconColor: '#eb4d5c',
color: '#eb4d5c',
lineHeight: '25px',
fontWeight: 300,
fontFamily: "'lato-regular', sans-serif",
fontSize: '14px',
'::placeholder': {
],
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',
fontWeight: 400
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 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);
}
});
}
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);
}
});
}
var submit_form_btn = $('#payment_button_with_creditcard');
@ -163,7 +167,7 @@ $(document).ready(function () {
if (parts.length === 2) return parts.pop().split(";").shift();
}
function submitBillingForm() {
function submitBillingForm(pmId) {
var billing_form = $('#billing-form');
var recurring_input = $('#id_generic_payment_form-recurring');
billing_form.append('<input type="hidden" name="generic_payment_form-product_name" value="' + $('#id_generic_payment_form-product_name').val() + '" />');
@ -174,11 +178,40 @@ $(document).ready(function () {
billing_form.append('<input type="hidden" name="generic_payment_form-recurring" value="' + (recurring_input.prop('checked') ? 'on' : '') + '" />');
}
billing_form.append('<input type="hidden" name="generic_payment_form-description" value="' + $('#id_generic_payment_form-description').val() + '" />');
billing_form.append('<input type="hidden" name="id_payment_method" value="' + pmId + '" />');
billing_form.submit();
}
var $form_new = $('#payment-form-new');
$form_new.submit(payWithStripe_new);
$form_new.submit(payWithPaymentIntent);
window.result = "";
window.card = "";
function payWithPaymentIntent(e) {
e.preventDefault();
function stripePMHandler(paymentMethod) {
// Insert the token ID into the form so it gets submitted to the server
console.log(paymentMethod);
$('#id_payment_method').val(paymentMethod.id);
submitBillingForm(paymentMethod.id);
}
stripe.createPaymentMethod({
type: 'card',
card: cardNumberElement,
})
.then(function(result) {
// Handle result.error or result.paymentMethod
window.result = result;
if(result.error) {
var errorElement = document.getElementById('card-errors');
errorElement.textContent = result.error.message;
} else {
console.log("created paymentMethod " + result.paymentMethod.id);
stripePMHandler(result.paymentMethod);
}
});
window.card = cardNumberElement;
}
function payWithStripe_new(e) {
e.preventDefault();
@ -197,7 +230,7 @@ $(document).ready(function () {
} else {
var process_text = "Processing";
if (typeof window.processing_text !== 'undefined') {
process_text = window.processing_text
process_text = window.processing_text;
}
$form_new.find('[type=submit]').html(process_text + ' <i class="fa fa-spinner fa-pulse"></i>');

View file

@ -92,50 +92,117 @@ $(document).ready(function() {
});
var create_vm_form = $('#virtual_machine_create_form');
create_vm_form.submit(function () {
$('#btn-create-vm').prop('disabled', true);
$.ajax({
url: create_vm_form.attr('action'),
type: 'POST',
data: create_vm_form.serialize(),
init: function(){
ok_btn = $('#createvm-modal-done-btn');
close_btn = $('#createvm-modal-close-btn');
ok_btn.addClass('btn btn-success btn-ok btn-wide hide');
close_btn.addClass('btn btn-danger btn-ok btn-wide hide');
},
success: function (data) {
fa_icon = $('.modal-icon > .fa');
modal_btn = $('#createvm-modal-done-btn');
$('#createvm-modal-title').text(data.msg_title);
$('#createvm-modal-body').html(data.msg_body);
if (data.redirect) {
modal_btn.attr('href', data.redirect).removeClass('hide');
} else {
modal_btn.attr('href', "");
}
if (data.status === true) {
fa_icon.attr('class', 'checkmark');
} else {
fa_icon.attr('class', 'fa fa-close');
modal_btn.attr('class', '').addClass('btn btn-danger btn-ok btn-wide');
}
},
error: function (xmlhttprequest, textstatus, message) {
if (window.isSubscription) {
create_vm_form.submit(function () {
$('#btn-create-vm').prop('disabled', true);
$.ajax({
url: create_vm_form.attr('action'),
type: 'POST',
data: create_vm_form.serialize(),
init: function () {
ok_btn = $('#createvm-modal-done-btn');
close_btn = $('#createvm-modal-close-btn');
ok_btn.addClass('btn btn-success btn-ok btn-wide hide');
close_btn.addClass('btn btn-danger btn-ok btn-wide hide');
},
success: function (data) {
fa_icon = $('.modal-icon > .fa');
modal_btn = $('#createvm-modal-done-btn');
if (data.showSCA) {
console.log("Show SCA");
var stripe = Stripe(data.STRIPE_PUBLISHABLE_KEY);
stripe.confirmCardPayment(data.payment_intent_secret).then(function (result) {
if (result.error) {
// Display error.message in your UI.
modal_btn.attr('href', data.error.redirect).removeClass('hide');
fa_icon.attr('class', 'fa fa-close');
modal_btn.attr('class', '').addClass('btn btn-danger btn-ok btn-wide');
$('#createvm-modal-title').text(data.error.msg_title);
$('#createvm-modal-body').html(data.error.msg_body);
} else {
// The payment has succeeded. Display a success message.
modal_btn.attr('href', data.success.redirect).removeClass('hide');
fa_icon.attr('class', 'checkmark');
$('#createvm-modal-title').text(data.success.msg_title);
$('#createvm-modal-body').html(data.success.msg_body);
}
});
$('#3Dsecure-modal').show();
} else {
$('#createvm-modal-title').text(data.msg_title);
$('#createvm-modal-body').html(data.msg_body);
if (data.redirect) {
modal_btn.attr('href', data.redirect).removeClass('hide');
} else {
modal_btn.attr('href', "");
}
if (data.status === true) {
fa_icon.attr('class', 'checkmark');
} else {
fa_icon.attr('class', 'fa fa-close');
modal_btn.attr('class', '').addClass('btn btn-danger btn-ok btn-wide');
}
}
},
error: function (xmlhttprequest, textstatus, message) {
fa_icon = $('.modal-icon > .fa');
fa_icon.attr('class', 'fa fa-close');
if (typeof(create_vm_error_message) !== 'undefined') {
if (typeof (create_vm_error_message) !== 'undefined') {
$('#createvm-modal-body').text(create_vm_error_message);
}
$('#btn-create-vm').prop('disabled', false);
$('#createvm-modal-close-btn').removeClass('hide');
}
}
});
return false;
});
return false;
});
} else {
create_vm_form.submit(placeOrderPaymentIntent);
function placeOrderPaymentIntent(e) {
e.preventDefault();
var stripe = Stripe(window.stripeKey);
stripe.confirmCardPayment(
window.paymentIntentSecret,
{
payment_method: window.pm_id
}
).then(function(result) {
window.result = result;
fa_icon = $('.modal-icon > .fa');
modal_btn = $('#createvm-modal-done-btn');
if (result.error) {
// Display error.message in your UI.
modal_btn.attr('href', error_url).removeClass('hide');
fa_icon.attr('class', 'fa fa-close');
modal_btn.attr('class', '').addClass('btn btn-danger btn-ok btn-wide');
$('#createvm-modal-title').text(error_title);
$('#createvm-modal-body').html(result.error.message + " " + error_msg);
} else {
// The payment has succeeded
// Display a success message
modal_btn.attr('href', success_url).removeClass('hide');
fa_icon.attr('class', 'checkmark');
$('#createvm-modal-title').text(success_title);
$('#createvm-modal-body').html(success_msg);
}
});
}
}
$('#createvm-modal').on('hidden.bs.modal', function () {
$(this).find('.modal-footer .btn').addClass('hide');
})
});
// Toggle subscription and one-time payments div
$('#li-one-time-charges').click(function() {
console.log("li-one-time-charges clicked");
$('#subscriptions').hide();
$('#one-time-charges').show();
});
$('#li-subscriptions').click(function() {
console.log("li-subscriptions clicked");
$('#one-time-charges').hide();
$('#subscriptions').show();
});
});
window.onload = function () {

View file

@ -29,7 +29,7 @@
<img class="svg-img" src="{% static 'hosting/img/key.svg' %}">
</div>
</a>
<a href="{% if has_invoices %}{% url 'hosting:invoices' %}{% else %}{% url 'hosting:orders' %}{% endif %}" class="hosting-dashboard-item">
<a href="{% url 'hosting:invoices' %}" class="hosting-dashboard-item">
<h2>{% trans "My Bills" %}</h2>
<div class="hosting-dashboard-image">
<img class="svg-img" src="{% static 'hosting/img/billing.svg' %}">

View file

@ -1,7 +1,11 @@
{% if messages %}
<ul class="list-unstyled msg-list">
{% for message in messages %}
<div class="alert {% if message.tags and message.tags == 'error' %} alert-danger {% else %} alert-{{message.tags}} {% endif %}">{{ message|safe }}</div>
{% if message.tags and 'error' in message.tags %}
<p class="card-warning-content card-warning-error">{{ message|safe }}</p>
{% elif message.tags %}
<div class="alert alert-{{message.tags}}">{{ message|safe }}</div>
{% endif %}
{% endfor %}
</ul>
{% endif %}

View file

@ -70,6 +70,9 @@
{{invoice.order.billing_address.postal_code}}<br>
{{invoice.order.billing_address.city}},
{{invoice.order.billing_address.country}}
{% if invoice.order.billing_address.vat_number %}
<br/>{% trans "VAT Number" %} {{invoice.order.billing_address.vat_number}}
{% endif %}
{% endif %}
</p>
</address>

View file

@ -15,47 +15,116 @@
<div class="dashboard-subtitle"></div>
</div>
<ul class="nav nav-tabs">
<li class="active" id="li-subscriptions"><a href="#">{% trans "Subscriptions" %}</a></li>
<li id="li-one-time-charges"><a href="#">{% trans "One-time payments" %}</a></li>
</ul>
<div class="subscriptions" id="subscriptions">
<table class="table table-switch">
<thead>
<tr>
<th>{% trans "VM ID" %}</th>
<th>{% trans "IP Address" %}</th>
<th>{% trans "IP Address" %}/{% trans "Product" %}</th>
<th>{% trans "Period" %}</th>
<th>{% trans "Amount" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for invoice in invoices %}
{% for inv_data in invs %}
<tr>
<td class="xs-td-inline" data-header="{% trans 'VM ID' %}">{{ invoice.order.vm_id }}</td>
<td class="xs-td-inline" data-header="{% trans 'IP Address' %}">{{ ips|get_value_from_dict:invoice.invoice_number|join:"<br/>" }}</td>
{% with period|get_value_from_dict:invoice.invoice_number as period_to_show %}
<td class="xs-td-inline" data-header="{% trans 'Period' %}">{{ period_to_show.period_start | date:'Y-m-d' }} &mdash; {{ period_to_show.period_end | date:'Y-m-d' }}</td>
{% endwith %}
<td class="xs-td-inline" data-header="{% trans 'Amount' %}">{{ invoice.total_in_chf|floatformat:2|intcomma }}</td>
<td class="text-right last-td">
<a class="btn btn-order-detail" href="{% url 'hosting:invoices' invoice.invoice_number %}">{% trans 'See Invoice' %}</a>
</td>
{{ inv_data | get_line_item_from_stripe_invoice }}
</tr>
{% endfor %}
</tbody>
</table>
{% if is_paginated %}
<div class="pagination">
<span class="page-links">
{% if page_obj.has_previous %}
<a href="{{request.path}}?page={{ page_obj.previous_page_number }}">{% trans "previous" %}</a>
{% endif %}
<span class="page-current">
{% trans "Page" %} {{ page_obj.number }} {% trans "of" %} {{ page_obj.paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="{{request.path}}?page={{ page_obj.next_page_number }}">{% trans "next" %}</a>
{% endif %}
</span>
</div>
{% if invs.has_other_pages %}
<ul class="pagination">
{% if invs.has_previous %}
{% if user_email %}
<li><a href="?page={{ invs.previous_page_number }}&user_email={{user_email}}">&laquo;</a></li>
{% else %}
<li><a href="?page={{ invs.previous_page_number }}">&laquo;</a></li>
{% endif %}
{% else %}
<li class="disabled"><span>&laquo;</span></li>
{% endif %}
{% for i in invs.paginator.page_range %}
{% if invs.number == i %}
<li class="active"><span>{{ i }} <span class="sr-only">(current)</span></span></li>
{% else %}
{% if user_email %}
<li><a href="?page={{ i }}&user_email={{user_email}}">{{ i }}</a></li>
{% else %}
<li><a href="?page={{ i }}">{{ i }}</a></li>
{% endif %}
{% endif %}
{% endfor %}
{% if invs.has_next %}
{% if user_email %}
<li><a href="?page={{ invs.next_page_number }}&user_email={{user_email}}">&raquo;</a></li>
{% else %}
<li><a href="?page={{ invs.next_page_number }}">&raquo;</a></li>
{% endif %}
{% else %}
<li class="disabled"><span>&raquo;</span></li>
{% endif %}
</ul>
{% endif %}
</div>
<div id="one-time-charges" class="one-time-charges" style="display: none;">
<table class="table table-switch">
<thead>
<tr>
<th>{% trans "Product" %}</th>
<th>{% trans "Date" %}</th>
<th>{% trans "Amount" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for ho, stripe_charge_data in invs_charge %}
<tr>
{{ ho.id | get_line_item_from_hosting_order_charge }}
</tr>
{% endfor %}
</tbody>
</table>
{% if invs_charge.has_other_pages %}
<ul class="pagination">
{% if invs_charge.has_previous %}
{% if user_email %}
<li><a href="?page={{ invs_charge.previous_page_number }}&user_email={{user_email}}">&laquo;</a></li>
{% else %}
<li><a href="?page={{ invs_charge.previous_page_number }}">&laquo;</a></li>
{% endif %}
{% else %}
<li class="disabled"><span>&laquo;</span></li>
{% endif %}
{% for i in invs_charge.paginator.page_range %}
{% if invs_charge.number == i %}
<li class="active"><span>{{ i }} <span class="sr-only">(current)</span></span></li>
{% else %}
{% if user_email %}
<li><a href="?page={{ i }}&user_email={{user_email}}">{{ i }}</a></li>
{% else %}
<li><a href="?page={{ i }}">{{ i }}</a></li>
{% endif %}
{% endif %}
{% endfor %}
{% if invs_charge.has_next %}
{% if user_email %}
<li><a href="?page={{ invs_charge.next_page_number }}&user_email={{user_email}}">&raquo;</a></li>
{% else %}
<li><a href="?page={{ invs_charge.next_page_number }}">&raquo;</a></li>
{% endif %}
{% else %}
<li class="disabled"><span>&raquo;</span></li>
{% endif %}
</ul>
{% endif %}
</div>
</div>
{% endblock %}

View file

@ -62,11 +62,17 @@
{{user.name}}<br>
{{order.billing_address.street_address}}, {{order.billing_address.postal_code}}<br>
{{order.billing_address.city}}, {{order.billing_address.country}}
{% if order.billing_address.vat_number %}
<br/>{% trans "VAT Number" %} {{order.billing_address.vat_number}}
{% endif %}
{% else %}
{% with request.session.billing_address_data as billing_address %}
{{billing_address.cardholder_name}}<br>
{{billing_address.street_address}}, {{billing_address.postal_code}}<br>
{{billing_address.city}}, {{billing_address.country}}
{% if billing_address.vat_number %}
<br/>{% trans "VAT Number" %} {{billing_address.vat_number}}
{% endif %}
{% endwith %}
{% endif %}
</p>
@ -212,7 +218,7 @@
{% csrf_token %}
<div class="row">
<div class="col-sm-8">
<div class="dcl-place-order-text">{% blocktrans with vm_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{ vm_price }} CHF/month{% endblocktrans %}.</div>
<div class="dcl-place-order-text">{% blocktrans with vm_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" you agree to our <a href="https://datacenterlight.ch/en-us/cms/terms-of-service/">Terms of Service</a> and this plan will charge your credit card account with {{ vm_price }} CHF/month.{% endblocktrans %}.</div>
</div>
<div class="col-sm-4 order-confirm-btn text-right">
<button class="btn choice-btn" id="btn-create-vm" data-href="{% url 'hosting:order-confirmation' %}" data-toggle="modal" data-target="#createvm-modal">

View file

@ -26,6 +26,16 @@
{% for field in form %}
{% bootstrap_field field show_label=False type='fields' bound_css_class='' %}
{% endfor %}
{% if form.instance.vat_number %}
{% if form.instance.vat_validation_status != "ch_vat" and form.instance.vat_validation_status != "not_needed" %}
{% if form.instance.vat_validation_status == "verified" %}
<span class="fa fa-fw fa-check-circle" aria-hidden="true" title='{% trans "Your VAT number has been verified" %}'></span>
{% elif form.instance.vat_validation_status == "pending" %}
<span class="fa fa-fw fa-info-circle" aria-hidden="true" title='{% trans "Your VAT number is under validation. VAT will be adjusted, once the validation is complete." %}'></span>
{% endif %}
{% endif %}
{% endif %}
<div class="form-group text-right">
<button type="submit" class="btn btn-vm-contact btn-wide" name="billing-form">{% trans "UPDATE" %}</button>
</div>

View file

@ -81,12 +81,10 @@
</td>
<td>
{% if user_key.private_key %}
<form action="{{ user_key.private_key.url }}">
<button style="color: #717274" type="submit" class="btn btn-default">
<a class="btn btn-default" style='color: #717274;' href="{{ user_key.private_key.url }}" download="{{user_key.private_key.name}}">
<span class="pc-only">{% trans "Download" %}</span>
<span class="mob-only"><i class="fa fa-download"></i></span>
</button>
</form>
</a>
{% endif %}
</td>
</tr>

View file

@ -46,7 +46,7 @@
<div class="vm-vmid">
<div class="vm-item-subtitle">{% trans "Current Pricing" %}</div>
<div class="vm-item-lg">{{order.price|floatformat:2|intcomma}} CHF/{% if order.generic_product %}{% trans order.generic_product.product_subscription_interval %}{% else %}{% trans "Month" %}{% endif %}</div>
<a class="btn btn-vm-invoice" href="{% url 'hosting:orders' order.pk %}">{% trans "See Invoice" %}</a>
<a class="btn btn-vm-invoice" href="{{inv_url}}" target="_blank">{% trans "See Invoice" %}</a>
</div>
</div>
<div class="vm-detail-item">

View file

@ -51,7 +51,7 @@ urlpatterns = [
name='choice_ssh_keys'),
url(r'delete_ssh_key/(?P<pk>\d+)/?$', SSHKeyDeleteView.as_view(),
name='delete_ssh_key'),
url(r'delete_card/(?P<pk>\d+)/?$', SettingsView.as_view(),
url(r'delete_card/(?P<pk>[\w\-]+)/$', SettingsView.as_view(),
name='delete_card'),
url(r'create_ssh_key/?$', SSHKeyCreateView.as_view(),
name='create_ssh_key'),

View file

@ -1,8 +1,10 @@
import logging
import uuid
from datetime import datetime
from urllib.parse import quote
from time import sleep
import stripe
from django import forms
from django.conf import settings
from django.contrib import messages
@ -10,7 +12,9 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.tokens import default_token_generator
from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.core.urlresolvers import reverse_lazy, reverse
from django.db.models import Q
from django.http import (
Http404, HttpResponseRedirect, HttpResponse, JsonResponse
)
@ -37,8 +41,10 @@ from stored_messages.settings import stored_messages_settings
from datacenterlight.cms_models import DCLCalculatorPluginModel
from datacenterlight.models import VMTemplate, VMPricing
from datacenterlight.utils import create_vm, get_cms_integration, check_otp
from hosting.models import UserCardDetail
from datacenterlight.utils import (
create_vm, get_cms_integration, check_otp, validate_vat_number
)
from hosting.models import UserCardDetail, StripeTaxRate
from membership.models import CustomUser, StripeCustomer
from opennebula_api.models import OpenNebulaManager
from opennebula_api.serializers import (
@ -54,11 +60,10 @@ from utils.hosting_utils import (
get_vm_price_with_vat, get_vm_price_for_given_vat, HostingUtils,
get_vat_rate_for_country
)
from utils.ldap_manager import LdapManager
from utils.mailer import BaseEmail
from utils.stripe_utils import StripeUtils
from utils.tasks import send_plain_email_task
from utils.ldap_manager import LdapManager
from utils.views import (
PasswordResetViewMixin, PasswordResetConfirmViewMixin, LoginViewMixin,
ResendActivationLinkViewMixin
@ -70,7 +75,7 @@ from .forms import (
from .mixins import ProcessVMSelectionMixin, HostingContextMixin
from .models import (
HostingOrder, HostingBill, HostingPlan, UserHostingKey, VMDetail,
GenericProduct, MonthlyHostingBill, HostingBillLineItem
GenericProduct, MonthlyHostingBill
)
logger = logging.getLogger(__name__)
@ -383,7 +388,7 @@ class PasswordResetConfirmView(HostingContextMixin,
user = CustomUser.objects.get(pk=uid)
opennebula_client = OpenNebulaManager(
email=user.email,
email=user.username,
password=user.password,
)
@ -475,7 +480,7 @@ class SSHKeyDeleteView(LoginRequiredMixin, DeleteView):
def delete(self, request, *args, **kwargs):
owner = self.request.user
manager = OpenNebulaManager(
email=owner.email,
email=owner.username,
password=owner.password
)
pk = self.kwargs.get('pk')
@ -529,7 +534,7 @@ class SSHKeyChoiceView(LoginRequiredMixin, View):
ssh_key.private_key.save(filename, content)
owner = self.request.user
manager = OpenNebulaManager(
email=owner.email,
email=owner.username,
password=owner.password
)
keys = get_all_public_keys(request.user)
@ -560,9 +565,11 @@ class SettingsView(LoginRequiredMixin, FormView):
stripe_customer = None
if hasattr(user, 'stripecustomer'):
stripe_customer = user.stripecustomer
cards_list = UserCardDetail.get_all_cards_list(
stripe_customer=stripe_customer
stripe_utils = StripeUtils()
cards_list_request = stripe_utils.get_available_payment_methods(
stripe_customer
)
cards_list = cards_list_request.get('response_object')
context.update({
'cards_list': cards_list,
'stripe_key': settings.STRIPE_API_PUBLIC_KEY
@ -573,108 +580,155 @@ class SettingsView(LoginRequiredMixin, FormView):
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
stripe_source_id=card_id
)
stripe_utils = StripeUtils()
card_details = stripe_utils.get_cards_details_from_payment_method(
card_id
)
if not card_details.get('response_object'):
logger.debug("Could not find card %s in stripe" % card_id)
messages.add_message(request, messages.ERROR,
_("Could not set a default card."))
return HttpResponseRedirect(reverse_lazy('hosting:settings'))
card_details_response = card_details['response_object']
msg = _(
("Your {brand} card ending in {last4} set as "
"default card").format(
brand=user_card_detail.brand,
last4=user_card_detail.last4
brand=card_details_response['brand'],
last4=card_details_response['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)
card = self.kwargs.get('pk')
stripe_utils = StripeUtils()
stripe_utils.dissociate_customer_card(
request.user.stripecustomer.stripe_id,
card
)
msg = _("Card deassociation successful")
messages.add_message(request, messages.SUCCESS, msg)
return HttpResponseRedirect(reverse_lazy('hosting:settings'))
form = self.get_form()
if form.is_valid():
if 'billing-form' in request.POST:
current_billing_address = self.request.user.billing_addresses.last()
billing_address_data = form.cleaned_data
billing_address_data.update({
'user': self.request.user.id
})
billing_address_user_form = UserBillingAddressForm(
instance=self.request.user.billing_addresses.first(),
instance=self.request.user.billing_addresses.order_by('-id').first(),
data=billing_address_data)
billing_address_user_form.save()
msg = _("Billing address updated successfully")
messages.add_message(request, messages.SUCCESS, msg)
billing_address = billing_address_user_form.save()
billing_address.stripe_tax_id = ''
billing_address.vat_number_validated_on = None
billing_address.vat_validation_status = ''
billing_address.save()
vat_number = billing_address_user_form.cleaned_data.get(
'vat_number').strip()
logger.debug("Vat number = %s" % vat_number)
if vat_number:
try:
stripe_customer = request.user.stripecustomer
except StripeCustomer.DoesNotExist as dne:
logger.debug(
"User %s does not have a stripecustomer. "
"Creating one." % request.user.email)
stripe_customer = StripeCustomer.get_or_create(
email=request.user.email,
token=None)
request.user.stripecustomer = stripe_customer
request.user.save()
validate_result = validate_vat_number(
stripe_customer_id=request.user.stripecustomer.stripe_id,
billing_address_id=billing_address.id,
is_user_ba=True
)
logger.debug("validate_result = %s" % str(validate_result))
if 'error' in validate_result and validate_result['error']:
messages.add_message(
request, messages.ERROR,
"VAT Number validation error: %s" % validate_result["error"],
extra_tags='error'
)
billing_address = current_billing_address
if billing_address:
billing_address.save()
email_data = {
'subject': "%s updated VAT number to %s but failed" %
(request.user.email, vat_number),
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': settings.DCL_ERROR_EMAILS_TO_LIST,
'body': "\n".join(
["%s=%s" % (k, v) for (k, v) in
validate_result.items()]),
}
else:
email_data = {
'subject': "%s updated VAT number to %s" % (
request.user.email, vat_number
),
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': settings.DCL_ERROR_EMAILS_TO_LIST,
'body': "\n".join(
["%s=%s" % (k, v) for (k, v) in
validate_result.items()]),
}
msg = _("Billing address updated successfully")
messages.add_message(request, messages.SUCCESS, msg)
send_plain_email_task.delay(email_data)
else:
msg = _("Billing address updated successfully")
messages.add_message(request, messages.SUCCESS, msg)
else:
token = form.cleaned_data.get('token')
id_payment_method = request.POST.get('id_payment_method', None)
stripe_utils = StripeUtils()
card_details = stripe_utils.get_cards_details_from_token(
token
card_details = stripe_utils.get_cards_details_from_payment_method(
id_payment_method
)
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
email=request.user.email, id_payment_method=id_payment_method
)
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
)
acc_result = stripe_utils.associate_customer_card(
request.user.stripecustomer.stripe_id,
id_payment_method,
set_as_default=True
)
if acc_result['response_object'] is None:
msg = _(
"Successfully associated the card with your account"
'An error occurred while associating the card.'
' Details: {details}'.format(
details=acc_result['error']
)
)
messages.add_message(request, messages.SUCCESS, msg)
messages.add_message(request, messages.ERROR, msg)
return self.render_to_response(self.get_context_data())
preferred = False
if stripe_customer.usercarddetail_set.count() == 0:
preferred = True
UserCardDetail.create(
stripe_customer=stripe_customer,
last4=card['last4'],
brand=card['brand'],
fingerprint=card['fingerprint'],
exp_month=card['exp_month'],
exp_year=card['exp_year'],
card_id=card['card_id'],
preferred=preferred
)
msg = _(
"Successfully associated the card with your account"
)
messages.add_message(request, messages.SUCCESS, msg)
return self.render_to_response(self.get_context_data())
else:
billing_address_data = form.cleaned_data
@ -687,7 +741,7 @@ class PaymentVMView(LoginRequiredMixin, FormView):
form_class = BillingAddressForm
def get_form_kwargs(self):
current_billing_address = self.request.user.billing_addresses.first()
current_billing_address = self.request.user.billing_addresses.last()
form_kwargs = super(PaymentVMView, self).get_form_kwargs()
if not current_billing_address:
return form_kwargs
@ -699,6 +753,7 @@ class PaymentVMView(LoginRequiredMixin, FormView):
'city': current_billing_address.city,
'postal_code': current_billing_address.postal_code,
'country': current_billing_address.country,
'vat_number': current_billing_address.vat_number
}
})
return form_kwargs
@ -889,7 +944,7 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
except VMDetail.DoesNotExist:
try:
manager = OpenNebulaManager(
email=owner.email, password=owner.password
email=owner.username, password=owner.password
)
vm = manager.get_vm(obj.vm_id)
context['vm'] = VirtualMachineSerializer(vm).data
@ -1014,7 +1069,13 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
billing_address_data = request.session.get('billing_address_data')
vm_template_id = template.get('id', 1)
stripe_api_cus_id = request.user.stripecustomer.stripe_id
if 'token' in self.request.session:
logger.debug("template=%s specs=%s stripe_customer_id=%s "
"billing_address_data=%s vm_template_id=%s "
"stripe_api_cus_id=%s" % (
template, specs, stripe_customer_id, billing_address_data,
vm_template_id, stripe_api_cus_id)
)
if 'id_payment_method' in self.request.session:
card_details = stripe_utils.get_cards_details_from_token(
request.session['token']
)
@ -1031,7 +1092,7 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
)
if not ucd:
acc_result = stripe_utils.associate_customer_card(
stripe_api_cus_id, request.session['token'],
stripe_api_cus_id, request.session['id_payment_method'],
set_as_default=True
)
if acc_result['response_object'] is None:
@ -1072,7 +1133,8 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
cpu = specs.get('cpu')
memory = specs.get('memory')
disk_size = specs.get('disk_size')
amount_to_be_charged = specs.get('total_price')
amount_to_be_charged = specs.get('price')
discount = specs.get('discount')
plan_name = StripeUtils.get_stripe_plan_name(
cpu=cpu,
memory=memory,
@ -1091,11 +1153,61 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
amount=amount_to_be_charged,
name=plan_name,
stripe_plan_id=stripe_plan_id)
# Create StripeTaxRate if applicable to the user
stripe_tax_rate = None
if specs["vat_percent"] > 0:
try:
stripe_tax_rate = StripeTaxRate.objects.get(
description="VAT for %s" % specs["vat_country"]
)
print("Stripe Tax Rate exists")
except StripeTaxRate.DoesNotExist as dne:
print("StripeTaxRate does not exist")
tax_rate_obj = stripe.TaxRate.create(
display_name="VAT",
description="VAT for %s" % specs["vat_country"],
jurisdiction=specs["vat_country"],
percentage=specs["vat_percent"] * 100,
inclusive=False,
)
stripe_tax_rate = StripeTaxRate.objects.create(
display_name=tax_rate_obj.display_name,
description=tax_rate_obj.description,
jurisdiction=tax_rate_obj.jurisdiction,
percentage=tax_rate_obj.percentage,
inclusive=False,
tax_rate_id=tax_rate_obj.id
)
logger.debug("Created StripeTaxRate %s" %
stripe_tax_rate.tax_rate_id)
subscription_result = stripe_utils.subscribe_customer_to_plan(
stripe_api_cus_id,
[{"plan": stripe_plan.get(
'response_object').stripe_plan_id}])
[{"plan": stripe_plan.get('response_object').stripe_plan_id}],
coupon=(discount['stripe_coupon_id']
if 'name' in discount and
discount['name'] is not None and
'ipv6' in discount['name'].lower() and
discount['stripe_coupon_id']
else ""),
tax_rates=[stripe_tax_rate.tax_rate_id] if stripe_tax_rate else [],
default_payment_method=request.session['id_payment_method']
)
stripe_subscription_obj = subscription_result.get('response_object')
latest_invoice = stripe.Invoice.retrieve(stripe_subscription_obj.latest_invoice)
ret = stripe.PaymentIntent.confirm(
latest_invoice.payment_intent
)
if ret.status == 'requires_source_action' or ret.status == 'requires_action':
pi = stripe.PaymentIntent.retrieve(
latest_invoice.payment_intent
)
context = {
'sid': stripe_subscription_obj.id,
'payment_intent_secret': pi.client_secret,
'STRIPE_PUBLISHABLE_KEY': settings.STRIPE_API_PUBLIC_KEY,
'showSCA': True
}
return JsonResponse(context)
# Check if the subscription was approved and is active
if (stripe_subscription_obj is None or
stripe_subscription_obj.status != 'active'):
@ -1134,6 +1246,7 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
user = {
'name': self.request.user.name,
'email': self.request.user.email,
'username': self.request.user.username,
'pass': self.request.user.password,
'request_scheme': request.scheme,
'request_host': request.get_host(),
@ -1177,7 +1290,7 @@ class OrdersHostingListView(LoginRequiredMixin, ListView):
return super(OrdersHostingListView, self).get(request, *args, **kwargs)
class InvoiceListView(LoginRequiredMixin, ListView):
class InvoiceListView(LoginRequiredMixin, TemplateView):
template_name = "hosting/invoices.html"
login_url = reverse_lazy('hosting:login')
context_object_name = "invoices"
@ -1185,10 +1298,14 @@ class InvoiceListView(LoginRequiredMixin, ListView):
ordering = '-created'
def get_context_data(self, **kwargs):
page = self.request.GET.get('page', 1)
context = super(InvoiceListView, self).get_context_data(**kwargs)
invs_page = None
invs_page_charges = None
if ('user_email' in self.request.GET
and self.request.user.email == settings.ADMIN_EMAIL):
user_email = self.request.GET['user_email']
context['user_email'] = '%s' % quote(user_email)
logger.debug(
"user_email = {}".format(user_email)
)
@ -1197,53 +1314,66 @@ class InvoiceListView(LoginRequiredMixin, ListView):
except CustomUser.DoesNotExist as dne:
logger.debug("User does not exist")
cu = self.request.user
mhbs = MonthlyHostingBill.objects.filter(customer__user=cu)
else:
mhbs = MonthlyHostingBill.objects.filter(
customer__user=self.request.user
)
ips_dict = {}
line_item_period_dict = {}
for mhb in mhbs:
invs = stripe.Invoice.list(customer=cu.stripecustomer.stripe_id,
count=100,
status='paid')
paginator = Paginator(invs.data, 10)
try:
vm_detail = VMDetail.objects.get(vm_id=mhb.order.vm_id)
ips_dict[mhb.invoice_number] = [vm_detail.ipv6, vm_detail.ipv4]
all_line_items = HostingBillLineItem.objects.filter(monthly_hosting_bill=mhb)
for line_item in all_line_items:
if line_item.get_item_detail_str() != "":
line_item_period_dict[mhb.invoice_number] = {
"period_start": line_item.period_start,
"period_end": line_item.period_end
}
break
except VMDetail.DoesNotExist as dne:
ips_dict[mhb.invoice_number] = ['--']
logger.debug("VMDetail for {} doesn't exist".format(
mhb.order.vm_id
))
context['ips'] = ips_dict
context['period'] = line_item_period_dict
return context
invs_page = paginator.page(page)
except PageNotAnInteger:
invs_page = paginator.page(1)
except EmptyPage:
invs_page = paginator.page(paginator.num_pages)
hosting_orders = HostingOrder.objects.filter(
customer=cu.stripecustomer).filter(
Q(subscription_id=None) | Q(subscription_id='')
).order_by('-created_at')
stripe_chgs = []
for ho in hosting_orders:
stripe_chgs.append({ho.id: stripe.Charge.retrieve(ho.stripe_charge_id)})
def get_queryset(self):
user = self.request.user
if ('user_email' in self.request.GET
and self.request.user.email == settings.ADMIN_EMAIL):
user_email = self.request.GET['user_email']
logger.debug(
"user_email = {}".format(user_email)
)
paginator_charges = Paginator(stripe_chgs, 10)
try:
cu = CustomUser.objects.get(email=user_email)
except CustomUser.DoesNotExist as dne:
logger.debug("User does not exist")
cu = self.request.user
self.queryset = MonthlyHostingBill.objects.filter(customer__user=cu)
invs_page_charges = paginator_charges.page(page)
except PageNotAnInteger:
invs_page_charges = paginator_charges.page(1)
except EmptyPage:
invs_page_charges = paginator_charges.page(paginator_charges.num_pages)
else:
self.queryset = MonthlyHostingBill.objects.filter(
customer__user=self.request.user
)
return super(InvoiceListView, self).get_queryset()
try:
invs = stripe.Invoice.list(
customer=self.request.user.stripecustomer.stripe_id,
count=100
)
paginator = Paginator(invs.data, 10)
try:
invs_page = paginator.page(page)
except PageNotAnInteger:
invs_page = paginator.page(1)
except EmptyPage:
invs_page = paginator.page(paginator.num_pages)
hosting_orders = HostingOrder.objects.filter(
customer=self.request.user.stripecustomer).filter(
Q(subscription_id=None) | Q(subscription_id='')
).order_by('-created_at')
stripe_chgs = []
for ho in hosting_orders:
stripe_chgs.append(
{ho: stripe.Charge.retrieve(ho.stripe_charge_id)})
paginator_charges = Paginator(stripe_chgs, 10)
try:
invs_page_charges = paginator_charges.page(page)
except PageNotAnInteger:
invs_page_charges = paginator_charges.page(1)
except EmptyPage:
invs_page_charges = paginator_charges.page(
paginator_charges.num_pages)
except Exception as ex:
logger.error(str(ex))
invs_page = None
context["invs"] = invs_page
context["invs_charge"] = invs_page_charges
return context
@method_decorator(decorators)
def get(self, request, *args, **kwargs):
@ -1323,7 +1453,7 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView):
# fallback to get it from the infrastructure
try:
manager = OpenNebulaManager(
email=self.request.user.email,
email=self.request.user.username,
password=self.request.user.password
)
vm = manager.get_vm(vm_id)
@ -1406,7 +1536,7 @@ class VirtualMachinesPlanListView(LoginRequiredMixin, ListView):
def get_queryset(self):
owner = self.request.user
manager = OpenNebulaManager(email=owner.email,
manager = OpenNebulaManager(email=owner.username,
password=owner.password)
try:
queryset = manager.get_vms()
@ -1567,7 +1697,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
owner = self.request.user
vm = None
manager = OpenNebulaManager(
email=owner.email,
email=owner.username,
password=owner.password
)
vm_id = self.kwargs.get('pk')
@ -1611,13 +1741,28 @@ class VirtualMachineView(LoginRequiredMixin, View):
context = None
try:
serializer = VirtualMachineSerializer(vm)
hosting_order = HostingOrder.objects.get(
vm_id=serializer.data['vm_id']
)
inv_url = None
if hosting_order.subscription_id:
stripe_obj = stripe.Invoice.list(
subscription=hosting_order.subscription_id,
count=1
)
inv_url = stripe_obj.data[0].hosted_invoice_url
elif hosting_order.stripe_charge_id:
stripe_obj = stripe.Charge.retrieve(
hosting_order.stripe_charge_id
)
inv_url = stripe_obj.receipt_url
context = {
'virtual_machine': serializer.data,
'order': HostingOrder.objects.get(
vm_id=serializer.data['vm_id']
),
'keys': UserHostingKey.objects.filter(user=request.user)
'order': hosting_order,
'keys': UserHostingKey.objects.filter(user=request.user),
'inv_url': inv_url
}
except Exception as ex:
logger.debug("Exception generated {}".format(str(ex)))
messages.error(self.request,
@ -1636,7 +1781,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
vm = self.get_object()
manager = OpenNebulaManager(
email=owner.email,
email=owner.username,
password=owner.password
)
try:
@ -1649,6 +1794,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
# Cancel Stripe subscription
stripe_utils = StripeUtils()
hosting_order = None
stripe_subscription_obj = None
try:
hosting_order = HostingOrder.objects.get(
vm_id=vm.id
@ -1663,7 +1809,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
error_msg = result.get('error')
logger.error(
'Error canceling subscription for {user} and vm id '
'{vm_id}'.format(user=owner.email, vm_id=vm.id)
'{vm_id}'.format(user=owner.username, vm_id=vm.id)
)
logger.error(error_msg)
admin_email_body['stripe_error_msg'] = error_msg
@ -1685,7 +1831,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
)
response['text'] = str(_('Error terminating VM')) + str(vm.id)
else:
for t in range(15):
for t in range(settings.MAX_TIME_TO_WAIT_FOR_VM_TERMINATE):
try:
manager.get_vm(vm.id)
except WrongIdError:
@ -1708,6 +1854,10 @@ class VirtualMachineView(LoginRequiredMixin, View):
)
break
else:
logger.debug(
'Sleeping 2 seconds for terminate action on VM %s' %
vm.id
)
sleep(2)
if not response['status']:
response['text'] = str(_("VM terminate action timed out. "
@ -1735,19 +1885,33 @@ class VirtualMachineView(LoginRequiredMixin, View):
email.send()
admin_email_body.update(response)
admin_email_body["customer_email"] = owner.email
admin_email_body["customer_username"] = owner.username
admin_email_body["VM_ID"] = vm.id
admin_email_body["VM_created_at"] = (str(hosting_order.created_at) if
hosting_order is not None
else "unknown")
admin_msg_sub = "VM and Subscription for VM {} and user: {}".format(
content = ""
total_amount = 0
if stripe_subscription_obj:
for line_item in stripe_subscription_obj["items"]["data"]:
total_amount += (line_item["quantity"] *
line_item.plan["amount"])
content += " %s => %s x %s => %s\n" % (
line_item.plan["name"], line_item["quantity"],
line_item.plan["amount"]/100,
(line_item["quantity"] * line_item.plan["amount"])/100
)
admin_email_body["subscription_amount"] = total_amount/100
admin_email_body["subscription_detail"] = content
admin_msg_sub = "VM and Subscription for VM {} and user: {}, {}".format(
vm.id,
owner.email
owner.email, owner.username
)
email_to_admin_data = {
'subject': ("Deleted " if response['status']
else "ERROR deleting ") + admin_msg_sub,
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': ['info@ungleich.ch'],
'to': ['dcl-orders@ungleich.ch'],
'body': "\n".join(
["%s=%s" % (k, v) for (k, v) in admin_email_body.items()]),
}
@ -1787,7 +1951,7 @@ class HostingBillDetailView(PermissionRequiredMixin, LoginRequiredMixin,
context = super(DetailView, self).get_context_data(**kwargs)
owner = self.request.user
manager = OpenNebulaManager(email=owner.email,
manager = OpenNebulaManager(email=owner.username,
password=owner.password)
# Get vms
queryset = manager.get_vms()

View file

@ -233,8 +233,17 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
ldap_manager.create_user(self.username, password=password,
firstname=first_name, lastname=last_name,
email=self.email)
self.in_ldap = True
self.save()
else:
# User exists already in LDAP, but with a dummy credential
# We are here implies that the user has successfully
# authenticated against Django db, and a corresponding user
# exists in LDAP.
# We just update the LDAP credentials once again, assuming it
# was set to a dummy value while migrating users from Django to
# LDAP
ldap_manager.change_password(self.username, password)
self.in_ldap = True
self.save()
def __str__(self): # __unicode__ on Python 2
return self.email
@ -268,7 +277,7 @@ class StripeCustomer(models.Model):
return "%s - %s" % (self.stripe_id, self.user.email)
@classmethod
def create_stripe_api_customer(cls, email=None, token=None,
def create_stripe_api_customer(cls, email=None, id_payment_method=None,
customer_name=None):
"""
This method creates a Stripe API customer with the given
@ -279,7 +288,8 @@ class StripeCustomer(models.Model):
stripe user.
"""
stripe_utils = StripeUtils()
stripe_data = stripe_utils.create_customer(token, email, customer_name)
stripe_data = stripe_utils.create_customer(
id_payment_method, email, customer_name)
if stripe_data.get('response_object'):
stripe_cus_id = stripe_data.get('response_object').get('id')
return stripe_cus_id
@ -287,7 +297,7 @@ class StripeCustomer(models.Model):
return None
@classmethod
def get_or_create(cls, email=None, token=None):
def get_or_create(cls, email=None, token=None, id_payment_method=None):
"""
Check if there is a registered stripe customer with that email
or create a new one

View file

@ -154,6 +154,8 @@ class OpenNebulaManager():
protocol=settings.OPENNEBULA_PROTOCOL)
)
raise ConnectionRefusedError
except Exception as ex:
logger.error(str(ex))
def _get_user_pool(self):
try:
@ -427,8 +429,12 @@ class OpenNebulaManager():
template_id = int(template_id)
try:
template_pool = self._get_template_pool()
if template_id in settings.UPDATED_TEMPLATES_DICT.keys():
template_id = settings.UPDATED_TEMPLATES_DICT[template_id]
return template_pool.get_by_id(template_id)
except:
except Exception as ex:
logger.debug("Template Id we are looking for : %s" % template_id)
logger.error(str(ex))
raise ConnectionRefusedError
def create_template(self, name, cores, memory, disk_size, core_price,

View file

@ -86,7 +86,7 @@ class VirtualMachineSerializer(serializers.Serializer):
}
try:
manager = OpenNebulaManager(email=owner.email,
manager = OpenNebulaManager(email=owner.username,
password=owner.password,
)
opennebula_id = manager.create_vm(template_id=template_id,

View file

@ -19,7 +19,7 @@ class VmCreateView(generics.ListCreateAPIView):
def get_queryset(self):
owner = self.request.user
manager = OpenNebulaManager(email=owner.email,
manager = OpenNebulaManager(email=owner.username,
password=owner.password)
# We may have ConnectionRefusedError if we don't have a
# connection to OpenNebula. For now, we raise ServiceUnavailable
@ -42,7 +42,7 @@ class VmDetailsView(generics.RetrieveUpdateDestroyAPIView):
def get_queryset(self):
owner = self.request.user
manager = OpenNebulaManager(email=owner.email,
manager = OpenNebulaManager(email=owner.username,
password=owner.password)
# We may have ConnectionRefusedError if we don't have a
# connection to OpenNebula. For now, we raise ServiceUnavailable
@ -54,7 +54,7 @@ class VmDetailsView(generics.RetrieveUpdateDestroyAPIView):
def get_object(self):
owner = self.request.user
manager = OpenNebulaManager(email=owner.email,
manager = OpenNebulaManager(email=owner.username,
password=owner.password)
# We may have ConnectionRefusedError if we don't have a
# connection to OpenNebula. For now, we raise ServiceUnavailable
@ -66,7 +66,7 @@ class VmDetailsView(generics.RetrieveUpdateDestroyAPIView):
def perform_destroy(self, instance):
owner = self.request.user
manager = OpenNebulaManager(email=owner.email,
manager = OpenNebulaManager(email=owner.username,
password=owner.password)
# We may have ConnectionRefusedError if we don't have a
# connection to OpenNebula. For now, we raise ServiceUnavailable

21
release.sh Executable file
View file

@ -0,0 +1,21 @@
#!/bin/sh
# Nico Schottelius, 2021-12-17
current=$(git describe --dirty)
last_tag=$(git describe --tags --abbrev=0)
registry=harbor.ungleich.svc.p10.k8s.ooo/ungleich-public
image_url=$registry/dynamicweb:${current}
if echo $current | grep -q -e 'dirty$'; then
echo Refusing to release a dirty tree build
exit 1
fi
if [ "$current" != "$last_tag" ]; then
echo "Last tag ($last_tag) is not current version ($current)"
echo "Only release proper versions"
exit 1
fi
docker tag dynamicweb:${current} ${image_url}
docker push ${image_url}

View file

@ -25,7 +25,7 @@ django-compressor==2.0
django-debug-toolbar==1.4
python-dotenv==0.10.3
django-extensions==1.6.7
django-filer==1.2.0
django-filer==2.1.2
django-filter==0.13.0
django-formtools==1.0
django-guardian==1.4.4
@ -79,11 +79,11 @@ requests==2.10.0
rjsmin==1.0.12
six==1.10.0
sqlparse==0.1.19
stripe==1.33.0
stripe==2.41.0
wheel==0.29.0
django-admin-honeypot==1.0.0
coverage==4.3.4
git+https://github.com/ungleich/python-oca.git#egg=python-oca
git+https://github.com/ungleich/python-oca.git#egg=oca
djangorestframework==3.6.3
flake8==3.3.0
python-memcached==1.58

View file

@ -134,8 +134,6 @@
digitalglarus.ch<br/>
hack4lgarus.ch<br/>
ipv6onlyhosting.com<br/>
ipv6onlyhosting.ch<br/>
ipv6onlyhosting.net<br/>
django-hosting.ch<br/>
rails-hosting.ch<br/>
node-hosting.ch<br/>

View file

@ -124,13 +124,14 @@ class BillingAddressForm(forms.ModelForm):
class Meta:
model = BillingAddress
fields = ['cardholder_name', 'street_address',
'city', 'postal_code', 'country']
'city', 'postal_code', 'country', 'vat_number']
labels = {
'cardholder_name': _('Cardholder Name'),
'street_address': _('Street Address'),
'city': _('City'),
'postal_code': _('Postal Code'),
'Country': _('Country'),
'VAT Number': _('VAT Number')
}
@ -142,7 +143,7 @@ class BillingAddressFormSignup(BillingAddressForm):
class Meta:
model = BillingAddress
fields = ['name', 'email', 'cardholder_name', 'street_address',
'city', 'postal_code', 'country']
'city', 'postal_code', 'country', 'vat_number']
labels = {
'name': 'Name',
'email': _('Email'),
@ -151,6 +152,7 @@ class BillingAddressFormSignup(BillingAddressForm):
'city': _('City'),
'postal_code': _('Postal Code'),
'Country': _('Country'),
'vat_number': _('VAT Number')
}
def clean_email(self):
@ -173,13 +175,14 @@ class UserBillingAddressForm(forms.ModelForm):
class Meta:
model = UserBillingAddress
fields = ['cardholder_name', 'street_address',
'city', 'postal_code', 'country', 'user']
'city', 'postal_code', 'country', 'user', 'vat_number']
labels = {
'cardholder_name': _('Cardholder Name'),
'street_address': _('Street Building'),
'city': _('City'),
'postal_code': _('Postal Code'),
'Country': _('Country'),
'vat_number': _('VAT Number'),
}

View file

@ -1,7 +1,10 @@
import decimal
import logging
import math
import subprocess
from django.conf import settings
from oca.pool import WrongIdError
from datacenterlight.models import VMPricing
@ -78,7 +81,8 @@ def get_vm_price(cpu, memory, disk_size, hdd_size=0, pricing_name='default'):
price = ((decimal.Decimal(cpu) * pricing.cores_unit_price) +
(decimal.Decimal(memory) * pricing.ram_unit_price) +
(decimal.Decimal(disk_size) * pricing.ssd_unit_price) +
(decimal.Decimal(hdd_size) * pricing.hdd_unit_price))
(decimal.Decimal(hdd_size) * pricing.hdd_unit_price) +
decimal.Decimal(settings.VM_BASE_PRICE))
cents = decimal.Decimal('.01')
price = price.quantize(cents, decimal.ROUND_HALF_UP)
return round(float(price), 2)
@ -101,18 +105,25 @@ def get_vm_price_for_given_vat(cpu, memory, ssd_size, hdd_size=0,
(decimal.Decimal(cpu) * pricing.cores_unit_price) +
(decimal.Decimal(memory) * pricing.ram_unit_price) +
(decimal.Decimal(ssd_size) * pricing.ssd_unit_price) +
(decimal.Decimal(hdd_size) * pricing.hdd_unit_price)
(decimal.Decimal(hdd_size) * pricing.hdd_unit_price) +
decimal.Decimal(settings.VM_BASE_PRICE)
)
discount_name = pricing.discount_name
discount_amount = round(float(pricing.discount_amount), 2)
vat = price * decimal.Decimal(vat_rate) * decimal.Decimal(0.01)
vat_percent = vat_rate
cents = decimal.Decimal('.01')
price = price.quantize(cents, decimal.ROUND_HALF_UP)
vat = vat.quantize(cents, decimal.ROUND_HALF_UP)
discount_amount_with_vat = decimal.Decimal(discount_amount) * (1 + decimal.Decimal(vat_rate) * decimal.Decimal(0.01))
discount_amount_with_vat = discount_amount_with_vat.quantize(cents, decimal.ROUND_HALF_UP)
discount = {
'name': pricing.discount_name,
'amount': round(float(pricing.discount_amount), 2)
'name': discount_name,
'amount': discount_amount,
'amount_with_vat': round(float(discount_amount_with_vat), 2),
'stripe_coupon_id': pricing.stripe_coupon_id
}
return (round(float(price), 2), round(float(vat), 2),
round(float(vat_percent), 2), discount)
@ -148,7 +159,8 @@ def get_vm_price_with_vat(cpu, memory, ssd_size, hdd_size=0,
(decimal.Decimal(cpu) * pricing.cores_unit_price) +
(decimal.Decimal(memory) * pricing.ram_unit_price) +
(decimal.Decimal(ssd_size) * pricing.ssd_unit_price) +
(decimal.Decimal(hdd_size) * pricing.hdd_unit_price)
(decimal.Decimal(hdd_size) * pricing.hdd_unit_price) +
decimal.Decimal(settings.VM_BASE_PRICE)
)
if pricing.vat_inclusive:
vat = decimal.Decimal(0)
@ -162,7 +174,8 @@ def get_vm_price_with_vat(cpu, memory, ssd_size, hdd_size=0,
vat = vat.quantize(cents, decimal.ROUND_HALF_UP)
discount = {
'name': pricing.discount_name,
'amount': round(float(pricing.discount_amount), 2)
'amount': round(float(pricing.discount_amount), 2),
'stripe_coupon_id': pricing.stripe_coupon_id
}
return (round(float(price), 2), round(float(vat), 2),
round(float(vat_percent), 2), discount)
@ -199,6 +212,16 @@ def get_vat_rate_for_country(country):
return 0
def get_ip_addresses(vm_id):
try:
vm_detail = VMDetail.objects.get(vm_id=vm_id)
return "%s <br/>%s" % (vm_detail.ipv6, vm_detail.ipv4)
except VMDetail.DoesNotExist as dne:
logger.error(str(dne))
logger.error("VMDetail for %s does not exist" % vm_id)
return "--"
class HostingUtils:
@staticmethod
def clear_items_from_list(from_list, items_list):

View file

@ -3,6 +3,7 @@ import hashlib
import random
import ldap3
import logging
import unicodedata
from django.conf import settings
@ -87,7 +88,7 @@ class LdapManager:
logger.debug("{uid} does not exist. Using it".format(uid=uidNumber))
self._set_max_uid(uidNumber)
try:
uid = user.encode("utf-8")
uid = user
conn.add("uid={uid},{customer_dn}".format(
uid=uid, customer_dn=settings.LDAP_CUSTOMER_DN
),
@ -101,7 +102,7 @@ class LdapManager:
"uidNumber": [str(uidNumber)],
"gidNumber": [str(settings.LDAP_CUSTOMER_GROUP_ID)],
"loginShell": ["/bin/bash"],
"homeDirectory": ["/home/{}".format(user).encode("utf-8")],
"homeDirectory": ["/home/{}".format(unicodedata.normalize('NFKD', user).encode('ascii','ignore'))],
"mail": email.encode("utf-8"),
"userPassword": [self._ssha_password(
password.encode("utf-8")
@ -266,7 +267,7 @@ class LdapManager:
logger.error(
"Error reading int value from {}. {}"
"Returning default value {} instead".format(
settings.LDAP_MAX_UID_PATH,
settings.LDAP_MAX_UID_FILE_PATH,
str(ve),
settings.LDAP_DEFAULT_START_UID
)

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2019-12-26 14:02
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('utils', '0007_auto_20191226_0610'),
]
operations = [
migrations.AddField(
model_name='billingaddress',
name='vat_validation_status',
field=models.CharField(blank=True, default='', max_length=25),
),
migrations.AddField(
model_name='userbillingaddress',
name='vat_validation_status',
field=models.CharField(blank=True, default='', max_length=25),
),
]

View file

@ -13,6 +13,11 @@ class BaseBillingAddress(models.Model):
city = models.CharField(max_length=50)
postal_code = models.CharField(max_length=50)
country = CountryField()
vat_number = models.CharField(max_length=100, default="", blank=True)
stripe_tax_id = models.CharField(max_length=100, default="", blank=True)
vat_number_validated_on = models.DateTimeField(blank=True, null=True)
vat_validation_status = models.CharField(max_length=25, default="",
blank=True)
class Meta:
abstract = True
@ -20,10 +25,18 @@ class BaseBillingAddress(models.Model):
class BillingAddress(BaseBillingAddress):
def __str__(self):
return "%s, %s, %s, %s, %s" % (
self.cardholder_name, self.street_address, self.city,
self.postal_code, self.country
)
if self.vat_number:
return "%s, %s, %s, %s, %s, %s %s %s %s" % (
self.cardholder_name, self.street_address, self.city,
self.postal_code, self.country, self.vat_number,
self.stripe_tax_id, self.vat_number_validated_on,
self.vat_validation_status
)
else:
return "%s, %s, %s, %s, %s" % (
self.cardholder_name, self.street_address, self.city,
self.postal_code, self.country
)
class UserBillingAddress(BaseBillingAddress):
@ -31,10 +44,18 @@ class UserBillingAddress(BaseBillingAddress):
current = models.BooleanField(default=True)
def __str__(self):
return "%s, %s, %s, %s, %s" % (
self.cardholder_name, self.street_address, self.city,
self.postal_code, self.country
)
if self.vat_number:
return "%s, %s, %s, %s, %s, %s %s %s %s" % (
self.cardholder_name, self.street_address, self.city,
self.postal_code, self.country, self.vat_number,
self.stripe_tax_id, self.vat_number_validated_on,
self.vat_validation_status
)
else:
return "%s, %s, %s, %s, %s" % (
self.cardholder_name, self.street_address, self.city,
self.postal_code, self.country
)
def to_dict(self):
return {
@ -43,6 +64,7 @@ class UserBillingAddress(BaseBillingAddress):
'City': self.city,
'Postal Code': self.postal_code,
'Country': self.country,
'VAT Number': self.vat_number
}

View file

@ -1,7 +1,9 @@
import logging
import re
import stripe
from django.conf import settings
from datacenterlight.models import StripePlan
stripe.api_key = settings.STRIPE_API_PRIVATE_KEY
@ -32,32 +34,33 @@ def handleStripeError(f):
logger.error(str(e))
return response
except stripe.error.RateLimitError as e:
logger.error(str(e))
response.update(
{'error': "Too many requests made to the API too quickly"})
return response
except stripe.error.InvalidRequestError as e:
logger.error(str(e))
response.update({'error': "Invalid parameters"})
response.update({'error': str(e._message)})
return response
except stripe.error.AuthenticationError as e:
# Authentication with Stripe's API failed
# (maybe you changed API keys recently)
logger.error(str(e))
response.update({'error': common_message})
response.update({'error': str(e)})
return response
except stripe.error.APIConnectionError as e:
logger.error(str(e))
response.update({'error': common_message})
response.update({'error': str(e)})
return response
except stripe.error.StripeError as e:
# maybe send email
logger.error(str(e))
response.update({'error': common_message})
response.update({'error': str(e)})
return response
except Exception as e:
# maybe send email
logger.error(str(e))
response.update({'error': common_message})
response.update({'error': str(e)})
return response
return handleProblems
@ -67,7 +70,7 @@ class StripeUtils(object):
CURRENCY = 'chf'
INTERVAL = 'month'
SUCCEEDED_STATUS = 'succeeded'
STRIPE_PLAN_ALREADY_EXISTS = 'Plan already exists'
RESOURCE_ALREADY_EXISTS_ERROR_CODE = 'resource_already_exists'
STRIPE_NO_SUCH_PLAN = 'No such plan'
PLAN_EXISTS_ERROR_MSG = 'Plan {} exists already.\nCreating a local StripePlan now.'
PLAN_DOES_NOT_EXIST_ERROR_MSG = 'Plan {} does not exist.'
@ -80,20 +83,31 @@ class StripeUtils(object):
customer.save()
@handleStripeError
def associate_customer_card(self, stripe_customer_id, token,
def associate_customer_card(self, stripe_customer_id, id_payment_method,
set_as_default=False):
customer = stripe.Customer.retrieve(stripe_customer_id)
card = customer.sources.create(source=token)
stripe.PaymentMethod.attach(
id_payment_method,
customer=stripe_customer_id,
)
if set_as_default:
customer.default_source = card.id
customer.invoice_settings.default_payment_method = id_payment_method
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()
if card_id.startswith("pm"):
logger.debug("PaymentMethod %s detached %s" % (card_id,
stripe_customer_id))
pm = stripe.PaymentMethod.retrieve(card_id)
stripe.PaymentMethod.detach(card_id)
pm.delete()
else:
logger.debug("card %s detached %s" % (card_id, stripe_customer_id))
card = customer.sources.retrieve(card_id)
card.delete()
@handleStripeError
def update_customer_card(self, customer_id, token):
@ -185,6 +199,24 @@ class StripeUtils(object):
}
return card_details
@handleStripeError
def get_cards_details_from_payment_method(self, payment_method_id):
payment_method = stripe.PaymentMethod.retrieve(payment_method_id)
# payment_method does not always seem to have a card with id
# if that is the case, fallback to payment_method_id for card_id
card_id = payment_method_id
if hasattr(payment_method.card, 'id'):
card_id = payment_method.card.id
card_details = {
'last4': payment_method.card.last4,
'brand': payment_method.card.brand,
'exp_month': payment_method.card.exp_month,
'exp_year': payment_method.card.exp_year,
'fingerprint': payment_method.card.fingerprint,
'card_id': card_id
}
return card_details
def check_customer(self, stripe_cus_api_id, user, token):
try:
customer = stripe.Customer.retrieve(stripe_cus_api_id)
@ -204,11 +236,11 @@ class StripeUtils(object):
return customer
@handleStripeError
def create_customer(self, token, email, name=None):
def create_customer(self, id_payment_method, email, name=None):
if name is None or name.strip() == "":
name = email
customer = self.stripe.Customer.create(
source=token,
payment_method=id_payment_method,
description=name,
email=email
)
@ -265,11 +297,17 @@ class StripeUtils(object):
stripe_plan_db_obj = StripePlan.objects.create(
stripe_plan_id=stripe_plan_id)
except stripe.error.InvalidRequestError as e:
if self.STRIPE_PLAN_ALREADY_EXISTS in str(e):
logger.error(str(e))
logger.error("error_code = %s" % str(e.__dict__))
if self.RESOURCE_ALREADY_EXISTS_ERROR_CODE in e.error.code:
logger.debug(
self.PLAN_EXISTS_ERROR_MSG.format(stripe_plan_id))
stripe_plan_db_obj = StripePlan.objects.create(
stripe_plan_db_obj, c = StripePlan.objects.get_or_create(
stripe_plan_id=stripe_plan_id)
if c:
logger.debug("Created stripe plan %s" % stripe_plan_id)
else:
logger.debug("Plan %s exists already" % stripe_plan_id)
return stripe_plan_db_obj
@handleStripeError
@ -297,10 +335,15 @@ class StripeUtils(object):
return return_value
@handleStripeError
def subscribe_customer_to_plan(self, customer, plans, trial_end=None):
def subscribe_customer_to_plan(self, customer, plans, trial_end=None,
coupon="", tax_rates=list(),
default_payment_method=""):
"""
Subscribes the given customer to the list of given plans
:param default_payment_method:
:param tax_rates:
:param coupon:
:param customer: The stripe customer identifier
:param plans: A list of stripe plans.
:param trial_end: An integer representing when the Stripe subscription
@ -314,10 +357,17 @@ class StripeUtils(object):
]
:return: The subscription StripeObject
"""
logger.debug("Subscribing %s to plan %s : coupon = %s" % (
customer, str(plans), str(coupon)
))
subscription_result = self.stripe.Subscription.create(
customer=customer, items=plans, trial_end=trial_end
customer=customer, items=plans, trial_end=trial_end,
coupon=coupon,
default_tax_rates=tax_rates,
payment_behavior='allow_incomplete',
default_payment_method=default_payment_method
)
logger.debug("Done subscribing")
return subscription_result
@handleStripeError
@ -348,7 +398,7 @@ class StripeUtils(object):
@staticmethod
def get_stripe_plan_id(cpu, ram, ssd, version, app='dcl', hdd=None,
price=None):
price=None, excl_vat=True):
"""
Returns the Stripe plan id string of the form
`dcl-v1-cpu-2-ram-5gb-ssd-10gb` based on the input parameters
@ -375,13 +425,16 @@ class StripeUtils(object):
plan=dcl_plan_string
)
if price is not None:
stripe_plan_id_string_with_price = '{}-{}chf'.format(
stripe_plan_id_string = '{}-{}chf'.format(
stripe_plan_id_string,
round(price, 2)
)
return stripe_plan_id_string_with_price
else:
return stripe_plan_id_string
if excl_vat:
stripe_plan_id_string = '{}-{}'.format(
stripe_plan_id_string,
"excl_vat"
)
return stripe_plan_id_string
@staticmethod
def get_vm_config_from_stripe_id(stripe_id):
@ -410,18 +463,27 @@ class StripeUtils(object):
@staticmethod
def get_stripe_plan_name(cpu, memory, disk_size, price):
def get_stripe_plan_name(cpu, memory, disk_size, price, excl_vat=True):
"""
Returns the Stripe plan name
:return:
"""
return "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD, " \
"{price} CHF".format(
cpu=cpu,
memory=memory,
disk_size=disk_size,
price=round(price, 2)
)
if excl_vat:
return "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD, " \
"{price} CHF Excl. VAT".format(
cpu=cpu,
memory=memory,
disk_size=disk_size,
price=round(price, 2)
)
else:
return "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD, " \
"{price} CHF".format(
cpu=cpu,
memory=memory,
disk_size=disk_size,
price=round(price, 2)
)
@handleStripeError
def set_subscription_meta_data(self, subscription_id, meta_data):
@ -434,3 +496,78 @@ class StripeUtils(object):
subscription = stripe.Subscription.retrieve(subscription_id)
subscription.metadata = meta_data
subscription.save()
@handleStripeError
def get_or_create_tax_id_for_user(self, stripe_customer_id, vat_number,
type="eu_vat", country=""):
tax_ids_list = stripe.Customer.list_tax_ids(
stripe_customer_id,
limit=100,
)
for tax_id_obj in tax_ids_list.data:
if self.compare_vat_numbers(tax_id_obj.value, vat_number):
logger.debug("tax id obj exists already")
return tax_id_obj
else:
logger.debug(
"{val1} is not equal to {val2} or {con1} not same as "
"{con2}".format(val1=tax_id_obj.value, val2=vat_number,
con1=tax_id_obj.country.lower(),
con2=country.lower().strip()))
logger.debug(
"tax id obj does not exist for {val}. Creating a new one".format(
val=vat_number
))
tax_id_obj = stripe.Customer.create_tax_id(
stripe_customer_id,
type=type,
value=vat_number,
)
return tax_id_obj
@handleStripeError
def get_payment_intent(self, amount, customer):
""" Create a stripe PaymentIntent of the given amount and return it
:param amount: the amount of payment_intent
:return:
"""
payment_intent_obj = stripe.PaymentIntent.create(
amount=amount,
currency='chf',
customer=customer,
setup_future_usage='off_session'
)
return payment_intent_obj
@handleStripeError
def get_available_payment_methods(self, customer):
""" Retrieves all payment methods of the given customer
:param customer: StripeCustomer object
:return: a list of available payment methods
"""
return_list = []
if customer is None:
return return_list
cu = stripe.Customer.retrieve(customer.stripe_id)
pms = stripe.PaymentMethod.list(
customer=customer.stripe_id,
type="card",
)
default_source = None
if cu.default_source:
default_source = cu.default_source
else:
default_source = cu.invoice_settings.default_payment_method
for pm in pms.data:
return_list.append({
'last4': pm.card.last4, 'brand': pm.card.brand, 'id': pm.id,
'exp_year': pm.card.exp_year,
'exp_month': '{:02d}'.format(pm.card.exp_month),
'preferred': pm.id == default_source
})
return return_list
def compare_vat_numbers(self, vat1, vat2):
_vat1 = vat1.replace(" ", "").replace(".", "").replace("-","")
_vat2 = vat2.replace(" ", "").replace(".", "").replace("-","")
return True if _vat1 == _vat2 else False

View file

@ -228,7 +228,7 @@ class SSHKeyCreateView(FormView):
if self.request.user.is_authenticated():
owner = self.request.user
manager = OpenNebulaManager(
email=owner.email,
email=owner.username,
password=owner.password
)
keys_to_save = get_all_public_keys(self.request.user)

View file

@ -319,3 +319,6 @@ IM",GBP,0.1,standard,
2019-12-17,,AD,EUR,0.045,standard,Andorra standard VAT (added manually)
2019-12-17,,TK,EUR,0.18,standard,Turkey standard VAT (added manually)
2019-12-17,,IS,EUR,0.24,standard,Iceland standard VAT (added manually)
2019-12-17,,FX,EUR,0.20,standard,France metropolitan standard VAT (added manually)
2020-01-04,,CY,EUR,0.19,standard,Cyprus standard VAT (added manually)
2019-01-04,,LI,EUR,0.077,standard,Liechtenstein standard VAT (added manually)

1 start_date stop_date territory_codes currency_code rate rate_type description
319
320
321
322
323
324

0
webhook/__init__.py Normal file
View file

3
webhook/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
webhook/apps.py Normal file
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class WebhookConfig(AppConfig):
name = 'webhook'

View file

@ -0,0 +1,81 @@
import logging
import stripe
from django.core.management.base import BaseCommand
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = '''creates webhook with the supplied arguments and returns the
webhook secret
'''
def add_arguments(self, parser):
parser.add_argument(
'--webhook_endpoint',
help="The url of the webhook endpoint that accepts the events "
"from stripe",
dest="webhook_endpoint",
required=False
)
parser.add_argument('--events_csv', dest="events_csv", required=False)
parser.add_argument('--webhook_id', dest="webhook_id", required=False)
parser.add_argument('--create', dest='create', action='store_true')
parser.add_argument('--list', dest='list', action='store_true')
parser.add_argument('--delete', dest='delete', action='store_true')
def handle(self, *args, **options):
wep_exists = False
if options['list']:
logger.debug("Listing webhooks")
we_list = stripe.WebhookEndpoint.list(limit=100)
for wep in we_list.data:
msg = wep.id + " -- " + ",".join(wep.enabled_events)
logger.debug(msg)
self.stdout.write(
self.style.SUCCESS(msg)
)
elif options['delete']:
logger.debug("Deleting webhook")
if 'webhook_id' in options:
stripe.WebhookEndpoint.delete(options['webhook_id'])
msg = "Deleted " + options['webhook_id']
logger.debug(msg)
self.stdout.write(
self.style.SUCCESS(msg)
)
else:
msg = "Supply webhook_id to delete a webhook"
logger.debug(msg)
self.stdout.write(
self.style.SUCCESS(msg)
)
exit(0)
elif options['create']:
logger.debug("Creating webhook")
try:
we_list = stripe.WebhookEndpoint.list(limit=100)
for wep in we_list.data:
if set(wep.enabled_events) == set(options['events_csv'].split(",")):
if wep.url == options['webhook_endpoint']:
logger.debug("We have this webhook already")
wep_exists = True
break
if wep_exists is False:
logger.debug(
"No webhook exists for {} at {}. Creatting a new endpoint "
"now".format(
options['webhook_endpoint'], options['events_csv']
)
)
wep = stripe.WebhookEndpoint.create(
url=options['webhook_endpoint'],
enabled_events=options['events_csv'].split(",")
)
self.stdout.write(
self.style.SUCCESS('Creation successful. '
'webhook_secret = %s' % wep.secret)
)
except Exception as e:
print(" *** Error occurred. Details {}".format(str(e)))

View file

3
webhook/models.py Normal file
View file

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

3
webhook/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

318
webhook/views.py Normal file
View file

@ -0,0 +1,318 @@
import datetime
import logging
import json
import stripe
# Create your views here.
from django.conf import settings
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from datacenterlight.views import do_provisioning, do_provisioning_generic
from membership.models import StripeCustomer
from hosting.models import IncompleteSubscriptions, IncompletePaymentIntents
from utils.models import BillingAddress, UserBillingAddress
from utils.tasks import send_plain_email_task
logger = logging.getLogger(__name__)
# Things to do for webhooks feature
# 1. Uninstall old version and install a more recent version of stripe
# ```
# source venv/bin/activate
# ./manage.py shell
# pip uninstall stripe
# pip install stripe==2.24.1
# ```
# 2. Create tax id updated webhook
# ```
# ./manage.py webhook --create \
# --webhook_endpoint https://datacenterlight.ch/en-us/webhooks/ \
# --events_csv customer.tax_id.updated
# ```
#
# 3. From the secret obtained in 2, setup an environment variable
# ```
# WEBHOOK_SECRET='whsec......'
# ```
@require_POST
@csrf_exempt
def handle_webhook(request):
payload = request.body
event = None
try:
if 'HTTP_STRIPE_SIGNATURE' in request.META:
sig_header = request.META['HTTP_STRIPE_SIGNATURE']
else:
logger.error("No HTTP_STRIPE_SIGNATURE header")
# Invalid payload
return HttpResponse(status=400)
event = stripe.Webhook.construct_event(
payload, sig_header, settings.WEBHOOK_SECRET
)
except ValueError as e:
# Invalid payload
err_msg = "FAILURE handle_invoice_webhook: Invalid payload details"
err_body = "Details %s" % str(e)
return handle_error(err_msg, err_body)
except stripe.error.SignatureVerificationError as e:
# Invalid signature
err_msg = "FAILURE handle_invoice_webhook: SignatureVerificationError"
err_body = "Details %s" % str(e)
return handle_error(err_msg, err_body)
# Do something with event
logger.debug("Passed signature verification")
if event.type == "customer.tax_id.updated":
logger.debug("Webhook Event: customer.tax_id.updated")
tax_id_obj = event.data.object
logger.debug("Tax_id %s is %s" % (tax_id_obj.id,
tax_id_obj.verification.status))
stripe_customer = None
try:
stripe_customer = StripeCustomer.objects.get(stripe_id=tax_id_obj.customer)
except StripeCustomer.DoesNotExist as dne:
logger.debug(
"StripeCustomer %s does not exist" % tax_id_obj.customer)
if tax_id_obj.verification.status == "verified":
b_addresses = BillingAddress.objects.filter(stripe_tax_id=tax_id_obj.id)
for b_address in b_addresses:
b_address.vat_validation_status = tax_id_obj.verification.status
b_address.vat_number_validated_on = datetime.datetime.now()
b_address.save()
ub_addresses = UserBillingAddress.objects.filter(stripe_tax_id=tax_id_obj.id)
for ub_address in ub_addresses:
ub_address.vat_validation_status = tax_id_obj.verification.status
ub_address.vat_number_validated_on = datetime.datetime.now()
ub_address.save()
email_data = {
'subject': "The VAT %s associated with %s was verified" %
(tax_id_obj.value, stripe_customer.user.email if stripe_customer else "unknown"),
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': settings.DCL_ERROR_EMAILS_TO_LIST,
'body': "The following objects were modified:\n".join(
'\n'.join([str(b_address) for b_address in b_addresses])
).join(
'\n'.join([str(ub_address) for ub_address in ub_addresses])
),
}
else:
logger.debug("Tax_id %s is %s" % (tax_id_obj.id,
tax_id_obj.verification.status))
email_data = {
'subject': "The VAT %s associated with %s was %s" %
(tax_id_obj.value, stripe_customer.user.email if stripe_customer else "unknown", tax_id_obj.verification.status),
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': settings.DCL_ERROR_EMAILS_TO_LIST,
'body': "Response = %s" % str(tax_id_obj),
}
send_plain_email_task.delay(email_data)
elif event.type == 'invoice.paid':
#More info: https://stripe.com/docs/billing/migration/strong-customer-authentication#scenario-1-handling-fulfillment
invoice_obj = event.data.object
logger.debug("Webhook Event: invoice.paid")
logger.debug("invoice_obj %s " % str(invoice_obj))
logger.debug("invoice_obj.paid = %s %s" % (invoice_obj.paid, type(invoice_obj.paid)))
logger.debug("invoice_obj.billing_reason = %s %s" % (invoice_obj.billing_reason, type(invoice_obj.billing_reason)))
# We should check for billing_reason == "subscription_create" but we
# check for "subscription_update"
# because we are using older api.
# See https://stripe.com/docs/upgrades?since=2015-07-13
# The billing_reason attribute of the invoice object now can take the
# value of subscription_create, indicating that it is the first
# invoice of a subscription. For older API versions,
# billing_reason=subscription_create is represented as
# subscription_update.
if (invoice_obj.paid and
invoice_obj.billing_reason == "subscription_update"):
logger.debug("""invoice_obj.paid and
invoice_obj.billing_reason == subscription_update""")
logger.debug("Start provisioning")
try:
logger.debug("Looking for subscription %s" %
invoice_obj.subscription)
stripe_subscription_obj = stripe.Subscription.retrieve(
invoice_obj.subscription)
try:
incomplete_sub = IncompleteSubscriptions.objects.get(
subscription_id=invoice_obj.subscription)
request = ""
soc = ""
card_details_response = ""
gp_details = ""
template = ""
specs = ""
billing_address_data = ""
if incomplete_sub.request:
request = json.loads(incomplete_sub.request)
if incomplete_sub.specs:
specs = json.loads(incomplete_sub.specs)
if incomplete_sub.stripe_onetime_charge:
soc = json.loads(incomplete_sub.stripe_onetime_charge)
if incomplete_sub.gp_details:
gp_details = json.loads(incomplete_sub.gp_details)
if incomplete_sub.card_details_response:
card_details_response = json.loads(
incomplete_sub.card_details_response)
if incomplete_sub.template:
template = json.loads(
incomplete_sub.template)
if incomplete_sub.billing_address_data:
billing_address_data = json.loads(
incomplete_sub.billing_address_data)
logger.debug("*******")
logger.debug(str(incomplete_sub))
logger.debug("*******")
logger.debug("1*******")
logger.debug(request)
logger.debug("2*******")
logger.debug(card_details_response)
logger.debug("3*******")
logger.debug(soc)
logger.debug("4*******")
logger.debug(gp_details)
logger.debug("5*******")
logger.debug(template)
logger.debug("6*******")
do_provisioning(
request=request,
stripe_api_cus_id=incomplete_sub.stripe_api_cus_id,
card_details_response=card_details_response,
stripe_subscription_obj=stripe_subscription_obj,
stripe_onetime_charge=soc,
gp_details=gp_details,
specs=specs,
vm_template_id=incomplete_sub.vm_template_id,
template=template,
billing_address_data=billing_address_data,
real_request=None
)
except IncompleteSubscriptions.DoesNotExist as ex:
logger.error(str(ex))
except IncompleteSubscriptions.MultipleObjectsReturned as ex:
logger.error(str(ex))
email_data = {
'subject': "IncompleteSubscriptions error",
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': settings.DCL_ERROR_EMAILS_TO_LIST,
'body': "Response = %s" % str(ex),
}
send_plain_email_task.delay(email_data)
except Exception as ex:
logger.error(str(ex))
email_data = {
'subject': "invoice.paid Webhook error",
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': settings.DCL_ERROR_EMAILS_TO_LIST,
'body': "Response = %s" % str(ex),
}
send_plain_email_task.delay(email_data)
elif event.type == 'invoice.payment_failed':
invoice_obj = event.data.object
logger.debug("Webhook Event: invoice.payment_failed")
logger.debug("invoice_obj %s " % str(invoice_obj))
if (invoice_obj.payment_failed and
invoice_obj.billing_reason == "subscription_update"):
logger.debug("Payment failed, inform the users")
elif event.type == 'payment_intent.succeeded':
payment_intent_obj = event.data.object
logger.debug("Webhook Event: payment_intent.succeeded")
logger.debug("payment_intent_obj %s " % str(payment_intent_obj))
try:
logger.debug("Looking for IncompletePaymentIntents %s " %
payment_intent_obj.id)
incomplete_pm = IncompletePaymentIntents.objects.get(
payment_intent_id=payment_intent_obj.id)
logger.debug("incomplete_pm = %s" % str(incomplete_pm.__dict__))
request = ""
soc = ""
card_details_response = ""
gp_details = ""
template = ""
billing_address_data = ""
if incomplete_pm.request:
request = json.loads(incomplete_pm.request)
logger.debug("request = %s" % str(request))
if incomplete_pm.stripe_charge_id:
soc = incomplete_pm.stripe_charge_id
logger.debug("stripe_onetime_charge = %s" % str(soc))
if incomplete_pm.gp_details:
gp_details = json.loads(incomplete_pm.gp_details)
logger.debug("gp_details = %s" % str(gp_details))
if incomplete_pm.card_details_response:
card_details_response = json.loads(
incomplete_pm.card_details_response)
logger.debug("card_details_response = %s" % str(card_details_response))
if incomplete_pm.billing_address_data:
billing_address_data = json.loads(
incomplete_pm.billing_address_data)
logger.debug("billing_address_data = %s" % str(billing_address_data))
logger.debug("1*******")
logger.debug(request)
logger.debug("2*******")
logger.debug(card_details_response)
logger.debug("3*******")
logger.debug(soc)
logger.debug("4*******")
logger.debug(gp_details)
logger.debug("5*******")
logger.debug(template)
logger.debug("6*******")
logger.debug(billing_address_data)
incomplete_pm.completed_at = datetime.datetime.now()
charges = ""
if len(payment_intent_obj.charges.data) > 0:
for d in payment_intent_obj.charges.data:
if charges == "":
charges = "%s" % d.id
else:
charges = "%s,%s" % (charges, d.id)
logger.debug("Charge ids = %s" % charges)
incomplete_pm.stripe_charge_id=charges
do_provisioning_generic(
request=request,
stripe_api_cus_id=incomplete_pm.stripe_api_cus_id,
card_details_response=card_details_response,
stripe_subscription_id=None,
stripe_charge_id=charges,
gp_details=gp_details,
billing_address_data=billing_address_data
)
incomplete_pm.save()
except IncompletePaymentIntents.DoesNotExist as ex:
logger.error(str(ex))
except (IncompletePaymentIntents.MultipleObjectsReturned,
Exception) as ex:
logger.error(str(ex))
email_data = {
'subject': "IncompletePaymentIntents error",
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': settings.DCL_ERROR_EMAILS_TO_LIST,
'body': "Response = %s" % str(ex),
}
send_plain_email_task.delay(email_data)
else:
logger.error("Unhandled event : " + event.type)
return HttpResponse(status=200)
def handle_error(error_msg, error_body):
logger.error("%s -- %s" % (error_msg, error_body))
email_to_admin_data = {
'subject': error_msg,
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': [settings.ADMIN_EMAIL],
'body': error_body,
}
send_plain_email_task.delay(email_to_admin_data)
return HttpResponse(status=400)