From 0b1c2cc1684acf37f53457863009741660857b31 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Sun, 15 Nov 2020 15:43:11 +0100 Subject: [PATCH] Cleanup code so that *most* test work again Still need to solve the downgrade test --- doc/uncloud-manual-2020-08-01.org | 15 +- uncloud/models.py | 1 + uncloud_net/models.py | 17 + uncloud_net/tests.py | 2 +- uncloud_pay/models.py | 16 +- uncloud_pay/models_prior_to_cleanup.py | 1003 ------------------------ uncloud_pay/tests-before-refactor.py | 721 ----------------- uncloud_pay/tests.py | 519 +----------- 8 files changed, 40 insertions(+), 2254 deletions(-) delete mode 100644 uncloud_pay/models_prior_to_cleanup.py delete mode 100644 uncloud_pay/tests-before-refactor.py diff --git a/doc/uncloud-manual-2020-08-01.org b/doc/uncloud-manual-2020-08-01.org index 6dd8fb2..8c8d7e5 100644 --- a/doc/uncloud-manual-2020-08-01.org +++ b/doc/uncloud-manual-2020-08-01.org @@ -236,16 +236,16 @@ VPNNetworks can be managed by all authenticated users. ** Milestones :uncloud: *** 1.1 (cleanup 1) -**** TODO Unify ValidationError, FieldError - define proper Exception +**** TODO [#C] Unify ValidationError, FieldError - define proper Exception - What do we use for model errors *** 1.0 (initial release) -**** TODO Initial Generic product support +**** TODO [#C] Initial Generic product support - Product -***** TODO Recurring product support -****** TODO Support replacing orders for updates +***** TODO [#C] Recurring product support +****** TODO [#C] Support replacing orders for updates ****** DONE [#A] Finish split of bill creation CLOSED: [2020-09-11 Fri 23:19] -****** TODO Test the new functions in the Order class +****** TODO [#C] Test the new functions in the Order class ****** Define the correct order replacement logic Assumption: - recurringperiods are 30days @@ -302,13 +302,14 @@ VPNNetworks can be managed by all authenticated users. - Total on bill: 30 CHF -****** TODO Note: ending date not set if replaced by default (implicit!) +****** TODO [#C] Note: ending date not set if replaced by default (implicit!) - Should the new order modify the old order on save()? ****** DONE Fix totally wrong bill dates in our test case CLOSED: [2020-09-09 Wed 01:00] - 2020 used instead of 2019 - Was due to existing test data ... -***** TODO Bill logic is still wrong +***** DONE Bill logic is still wrong + CLOSED: [2020-11-05 Thu 18:58] - Bill starting_date is the date of the first order - However first encountered order does not have to be the earliest in the bill! diff --git a/uncloud/models.py b/uncloud/models.py index 5a65f1c..9f977e8 100644 --- a/uncloud/models.py +++ b/uncloud/models.py @@ -3,6 +3,7 @@ from django.db.models import JSONField, Q from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django.core.validators import MinValueValidator, MaxValueValidator +from django.core.exceptions import FieldError from uncloud import COUNTRIES diff --git a/uncloud_net/models.py b/uncloud_net/models.py index c9c8bc3..509bb61 100644 --- a/uncloud_net/models.py +++ b/uncloud_net/models.py @@ -196,11 +196,27 @@ class ReverseDNSEntry(models.Model): name = models.CharField(max_length=253, null=False) + @property + def reverse_pointer(self): + return ipaddress.ip_address(self.ip_address).reverse_pointer + + def implement(self): + """ + The implement function implements the change + """ + + # Get all DNS entries (?) / update this DNS entry + # convert to DNS name + # + pass + + def save(self, *args, **kwargs): # Product.objects.filter(config__parameters__contains='reverse_dns_network') # FIXME: check if order is still active / not replaced allowed = False + product = None for order in Order.objects.filter(config__parameters__reverse_dns_network__isnull=False, owner=self.owner): @@ -211,6 +227,7 @@ class ReverseDNSEntry(models.Model): if addr in net: allowed = True + product = order.product break diff --git a/uncloud_net/tests.py b/uncloud_net/tests.py index 974f8dd..4491551 100644 --- a/uncloud_net/tests.py +++ b/uncloud_net/tests.py @@ -9,7 +9,7 @@ from .views import * from .models import * from uncloud_pay.models import BillingAddress, Order - +from uncloud.models import UncloudNetwork class UncloudNetworkTests(TestCase): def test_invalid_IPv4_network(self): diff --git a/uncloud_pay/models.py b/uncloud_pay/models.py index 353ab94..96fc8ec 100644 --- a/uncloud_pay/models.py +++ b/uncloud_pay/models.py @@ -209,7 +209,7 @@ class PaymentMethod(models.Model): pass # See https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices-enum-types -class RecurringPeriodChoices(models.IntegerChoices): +class RecurringPeriodDefaultChoices(models.IntegerChoices): """ This is an old class and being superseeded by the database model below """ @@ -234,7 +234,7 @@ class RecurringPeriod(models.Model): @classmethod def populate_db_defaults(cls): - for (seconds, name) in RecurringPeriodChoices.choices: + for (seconds, name) in RecurringPeriodDefaultChoices.choices: obj, created = cls.objects.get_or_create(name=name, defaults={ 'duration_seconds': seconds }) @@ -470,7 +470,7 @@ class Product(models.Model): @property def recurring_orders(self): - return self.orders.order_by('id').exclude(recurring_period=RecurringPeriod.ONE_TIME) + return self.orders.order_by('id').exclude(recurring_period=RecurringPeriod.objects.get(name="ONE_TIME")) @property def last_recurring_order(self): @@ -478,7 +478,7 @@ class Product(models.Model): @property def one_time_orders(self): - return self.orders.order_by('id').filter(recurring_period=RecurringPeriod.ONE_TIME) + return self.orders.order_by('id').filter(recurring_period=RecurringPeriod.objects.get(name="ONE_TIME")) @property def last_one_time_order(self): @@ -503,13 +503,13 @@ class Product(models.Model): billing_address=billing_address, starting_date=when_to_start, price=self.one_time_price, - recurring_period=RecurringPeriod.ONE_TIME, + recurring_period=RecurringPeriod.objects.get(name="ONE_TIME"), description=str(self)) self.orders.add(one_time_order) else: one_time_order = None - if recurring_period != RecurringPeriod.ONE_TIME: + if recurring_period != RecurringPeriod.objects.get(name="ONE_TIME"): if one_time_order: recurring_order = Order.objects.create(owner=self.owner, billing_address=billing_address, @@ -827,7 +827,7 @@ class Order(models.Model): @property def is_recurring(self): - return not self.recurring_period == RecurringPeriod.ONE_TIME + return not self.recurring_period == RecurringPeriod.objects.get(name="ONE_TIME") @property def is_one_time(self): @@ -857,6 +857,8 @@ class Order(models.Model): (new_order.one_time_price, new_order.recurring_price, new_order.config) = new_order.calculate_prices_and_config() + + new_order.replaces = self new_order.save() diff --git a/uncloud_pay/models_prior_to_cleanup.py b/uncloud_pay/models_prior_to_cleanup.py deleted file mode 100644 index 55ccffb..0000000 --- a/uncloud_pay/models_prior_to_cleanup.py +++ /dev/null @@ -1,1003 +0,0 @@ -from django.db import models -from django.db.models import Q -from django.contrib.auth import get_user_model -from django.utils.translation import gettext_lazy as _ -from django.core.validators import MinValueValidator -from django.utils import timezone -from django.core.exceptions import ObjectDoesNotExist, ValidationError - -import uuid -import logging -from functools import reduce -import itertools -from math import ceil -from datetime import timedelta -from calendar import monthrange -from decimal import Decimal - -import uncloud_pay.stripe -from uncloud_pay.helpers import beginning_of_month, end_of_month -from uncloud_pay import AMOUNT_DECIMALS, AMOUNT_MAX_DIGITS, COUNTRIES -from uncloud.models import UncloudModel, UncloudStatus - -from decimal import Decimal -import decimal - -# Used to generate bill due dates. -BILL_PAYMENT_DELAY=timedelta(days=10) - -# Initialize logger. -logger = logging.getLogger(__name__) - -# See https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices-enum-types -class RecurringPeriod(models.IntegerChoices): - PER_365D = 365*24*3600, _('Per 365 days') - PER_30D = 30*24*3600, _('Per 30 days') - PER_WEEK = 7*24*3600, _('Per Week') - PER_DAY = 24*3600, _('Per Day') - PER_HOUR = 3600, _('Per Hour') - PER_MINUTE = 60, _('Per Minute') - PER_SECOND = 1, _('Per Second') - ONE_TIME = 0, _('Onetime') - - -class CountryField(models.CharField): - def __init__(self, *args, **kwargs): - kwargs.setdefault('choices', COUNTRIES) - kwargs.setdefault('default', 'CH') - kwargs.setdefault('max_length', 2) - - super(CountryField, self).__init__(*args, **kwargs) - - def get_internal_type(self): - return "CharField" - -def get_balance_for_user(user): - bills = reduce( - lambda acc, entry: acc + entry.total, - Bill.objects.filter(owner=user), - 0) - payments = reduce( - lambda acc, entry: acc + entry.amount, - Payment.objects.filter(owner=user), - 0) - return payments - bills - -class StripeCustomer(models.Model): - owner = models.OneToOneField( get_user_model(), - primary_key=True, - on_delete=models.CASCADE) - stripe_id = models.CharField(max_length=32) - -### -# Payments and Payment Methods. - -class Payment(models.Model): - uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - - owner = models.ForeignKey(get_user_model(), - on_delete=models.CASCADE) - - amount = models.DecimalField( - default=0.0, - max_digits=AMOUNT_MAX_DIGITS, - decimal_places=AMOUNT_DECIMALS, - validators=[MinValueValidator(0)]) - - source = models.CharField(max_length=256, - choices = ( - ('wire', 'Wire Transfer'), - ('stripe', 'Stripe'), - ('voucher', 'Voucher'), - ('referral', 'Referral'), - ('unknown', 'Unknown') - ), - default='unknown') - timestamp = models.DateTimeField(editable=False, auto_now_add=True) - - # We override save() in order to active products awaiting payment. - def save(self, *args, **kwargs): - # _state.adding is switched to false after super(...) call. - being_created = self._state.adding - - unpaid_bills_before_payment = Bill.get_unpaid_for(self.owner) - super(Payment, self).save(*args, **kwargs) # Save payment in DB. - unpaid_bills_after_payment = Bill.get_unpaid_for(self.owner) - - newly_paid_bills = list( - set(unpaid_bills_before_payment) - set(unpaid_bills_after_payment)) - for bill in newly_paid_bills: - bill.activate_products() - - -class PaymentMethod(models.Model): - uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - owner = models.ForeignKey(get_user_model(), - on_delete=models.CASCADE, - editable=False) - source = models.CharField(max_length=256, - choices = ( - ('stripe', 'Stripe'), - ('unknown', 'Unknown'), - ), - default='stripe') - description = models.TextField() - primary = models.BooleanField(default=False, editable=False) - - # Only used for "Stripe" source - stripe_payment_method_id = models.CharField(max_length=32, blank=True, null=True) - stripe_setup_intent_id = models.CharField(max_length=32, blank=True, null=True) - - @property - def stripe_card_last4(self): - if self.source == 'stripe' and self.active: - payment_method = uncloud_pay.stripe.get_payment_method( - self.stripe_payment_method_id) - return payment_method.card.last4 - else: - return None - - @property - def active(self): - if self.source == 'stripe' and self.stripe_payment_method_id != None: - return True - else: - return False - - def charge(self, amount): - if not self.active: - raise Exception('This payment method is inactive.') - - if amount < 0: # Make sure we don't charge negative amount by errors... - raise Exception('Cannot charge negative amount.') - - if self.source == 'stripe': - stripe_customer = StripeCustomer.objects.get(owner=self.owner).stripe_id - stripe_payment = uncloud_pay.stripe.charge_customer( - amount, stripe_customer, self.stripe_payment_method_id) - if 'paid' in stripe_payment and stripe_payment['paid'] == False: - raise Exception(stripe_payment['error']) - else: - payment = Payment.objects.create( - owner=self.owner, source=self.source, amount=amount) - - return payment - else: - raise Exception('This payment method is unsupported/cannot be charged.') - - def set_as_primary_for(self, user): - methods = PaymentMethod.objects.filter(owner=user, primary=True) - for method in methods: - print(method) - method.primary = False - method.save() - - self.primary = True - self.save() - - def get_primary_for(user): - methods = PaymentMethod.objects.filter(owner=user) - for method in methods: - # Do we want to do something with non-primary method? - if method.active and method.primary: - return method - - return None - - class Meta: - # TODO: limit to one primary method per user. - # unique_together is no good since it won't allow more than one - # non-primary method. - pass - -### -# Bills. - -class BillingAddress(models.Model): - uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) - - organization = models.CharField(max_length=100) - name = models.CharField(max_length=100) - street = models.CharField(max_length=100) - city = models.CharField(max_length=50) - postal_code = models.CharField(max_length=50) - country = CountryField(blank=True) - vat_number = models.CharField(max_length=100, default="", blank=True) - - @staticmethod - def get_addresses_for(user): - return BillingAddress.objects.filter(owner=user) - - @classmethod - def get_preferred_address_for(cls, user): - addresses = cls.get_addresses_for(user) - if len(addresses) == 0: - return None - else: - # TODO: allow user to set primary/preferred address - return addresses[0] - - def __str__(self): - return "{}, {}, {} {}, {}".format( - self.name, self.street, self.postal_code, self.city, - self.country) - -# Populated with the import-vat-numbers django command. -class VATRate(models.Model): - start_date = models.DateField(blank=True, null=True) - stop_date = models.DateField(blank=True, null=True) - territory_codes = models.TextField(blank=True, default='') - currency_code = models.CharField(max_length=10) - rate = models.FloatField() - rate_type = models.TextField(blank=True, default='') - description = models.TextField(blank=True, default='') - - @staticmethod - def get_for_country(country_code): - vat_rate = None - try: - vat_rate = VATRate.objects.get( - territory_codes=country_code, start_date__isnull=False, stop_date=None - ) - return vat_rate.rate - except VATRate.DoesNotExist as dne: - logger.debug(str(dne)) - logger.debug("Did not find VAT rate for %s, returning 0" % country_code) - return 0 - -class BillNico(models.Model): - """ FIXME: - Bill needs to be unique in the triple (owner, year, month) - """ - - uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - owner = models.ForeignKey(get_user_model(), - on_delete=models.CASCADE) - - creation_date = models.DateTimeField(auto_now_add=True) - starting_date = models.DateTimeField() - ending_date = models.DateTimeField() - due_date = models.DateField() - - valid = models.BooleanField(default=True) - - @staticmethod - def create_all_bills(): - for owner in get_user_model().objects.all(): - # mintime = time of first order - # maxtime = time of last order - # iterate month based through it - pass - - def assign_orders_to_bill(self, owner, year, month): - """ - Generate a bill for the specific month of a user. - - First handle all one time orders - - FIXME: - - - limit this to active users in the future! (2020-05-23) - """ - - """ - Find all one time orders that have a starting date that falls into this month - recurring_period=RecurringPeriod.ONE_TIME, - - Can we do this even for recurring / all of them - - """ - - # FIXME: add something to check whether the order should be billed at all - i.e. a marker that - # disables searching -> optimization for later - for order in Order.objects.filter(Q(starting_date__gte=self.starting_date), - Q(starting_date__lte=self.ending_date), - owner=owner): - - order.bill.add(self) - - - """ - Find all recurring orders that did not start in this time frame, but need - to be billed in this time frame. - - This is: - - order starting time before our starting time - - order start time + (x * (the_period)) is inside our time frame, x must be integer - test cases: - + 365days: - time_since_last_billed = self.starting_or_ending_date - order.last_bill_date - periods = - [ we could in theory add this as a property to the order: next - """ - for order in Order.objects.filter(~Q(recurring_period=RecurringPeriod.ONE_TIME), - Q(starting_date__lt=self.starting_date), - owner=owner): - - if order.recurring_period > 0: # avoid div/0 - these are one time payments - - # How much time will have passed by the end of the billing cycle - td = self.ending_date - order.starting_date - - # How MANY times it will have been used by then - used_times = ceil(td / timedelta(seconds=order.recurring_period)) - - billed_times = len(order.bills) - - # How many times it WAS billed -- can also be inferred from the bills that link to it! - if used_times > billed_times: - billing_times = used_times - billed_times - - # ALSO REGISTER THE TIME PERIOD! -# order. - pass - - -class Bill(models.Model): - uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - owner = models.ForeignKey(get_user_model(), - on_delete=models.CASCADE) - - creation_date = models.DateTimeField(auto_now_add=True) - starting_date = models.DateTimeField() - ending_date = models.DateTimeField() - due_date = models.DateField() - - valid = models.BooleanField(default=True) - - # Trigger product activation if bill paid at creation (from balance). - def save(self, *args, **kwargs): - super(Bill, self).save(*args, **kwargs) - if not self in Bill.get_unpaid_for(self.owner): - self.activate_products() - - @property - def reference(self): - return "{}-{}".format( - self.owner.username, - self.creation_date.strftime("%Y-%m-%d-%H%M")) - - @property - def records(self): - bill_records = [] - orders = Order.objects.filter(bill=self) - for order in orders: - bill_record = BillRecord(self, order) - bill_records.append(bill_record) - - return bill_records - - @property - def amount(self): - return reduce(lambda acc, record: acc + record.amount, self.records, 0) - - @property - def vat_amount(self): - return reduce(lambda acc, record: acc + record.vat_amount, self.records, 0) - - @property - def total(self): - return self.amount + self.vat_amount - - @property - def final(self): - # A bill is final when its ending date is passed, or when all of its - # orders have been terminated. - every_order_terminated = True - billing_period_is_over = self.ending_date < timezone.now() - for order in self.order_set.all(): - every_order_terminated = every_order_terminated and order.is_terminated - - return billing_period_is_over or every_order_terminated - - def activate_products(self): - for order in self.order_set.all(): - # FIXME: using __something might not be a good idea. - for product_class in Product.__subclasses__(): - for product in product_class.objects.filter(order=order): - if product.status == UncloudStatus.AWAITING_PAYMENT: - product.status = UncloudStatus.PENDING - product.save() - - @property - def billing_address(self): - orders = Order.objects.filter(bill=self) - # The genrate_for method makes sure all the orders of a bill share the - # same billing address. TODO: It would be nice to enforce that somehow... - if orders: - return orders[0].billing_address - else: - return None - - # TODO: split this huuuge method! - @staticmethod - def generate_for(year, month, user): - # /!\ We exclusively work on the specified year and month. - generated_bills = [] - - # Default values for next bill (if any). - starting_date=beginning_of_month(year, month) - ending_date=end_of_month(year, month) - creation_date=timezone.now() - - # Select all orders active on the request period (i.e. starting on or after starting_date). - orders = Order.objects.filter( - Q(ending_date__gte=starting_date) | Q(ending_date__isnull=True), - owner=user) - - # Check if there is already a bill covering the order and period pair: - # * Get latest bill by ending_date: previous_bill.ending_date - # * For monthly bills: if previous_bill.ending_date is before - # (next_bill) ending_date, a new bill has to be generated. - # * For yearly bill: if previous_bill.ending_date is on working - # month, generate new bill. - unpaid_orders = { 'monthly_or_less': [], 'yearly': {} } - for order in orders: - try: - previous_bill = order.bill.latest('ending_date') - except ObjectDoesNotExist: - previous_bill = None - - # FIXME: control flow is confusing in this block. - # if order.recurring_period == RecurringPeriod.PER_YEAR: - # # We ignore anything smaller than a day in here. - # next_yearly_bill_start_on = None - # if previous_bill == None: - # next_yearly_bill_start_on = order.starting_date - # elif previous_bill.ending_date <= ending_date: - # next_yearly_bill_start_on = (previous_bill.ending_date + timedelta(days=1)) - - # # Store for bill generation. One bucket per day of month with a starting bill. - # # bucket is a reference here, no need to reassign. - # if next_yearly_bill_start_on: - # # We want to group orders by date but keep using datetimes. - # next_yearly_bill_start_on = next_yearly_bill_start_on.replace( - # minute=0, hour=0, second=0, microsecond=0) - # bucket = unpaid_orders['yearly'].get(next_yearly_bill_start_on) - # if bucket == None: - # unpaid_orders['yearly'][next_yearly_bill_start_on] = [order] - # else: - # unpaid_orders['yearly'][next_yearly_bill_start_on] = bucket + [order] - # else: - # if previous_bill == None or previous_bill.ending_date < ending_date: - # unpaid_orders['monthly_or_less'].append(order) - - # Handle working month's billing. - if len(unpaid_orders['monthly_or_less']) > 0: - # TODO: PREPAID billing is not supported yet. - prepaid_due_date = min(creation_date, starting_date) + BILL_PAYMENT_DELAY - postpaid_due_date = max(creation_date, ending_date) + BILL_PAYMENT_DELAY - - # There should not be any bill linked to orders with different - # billing addresses. - per_address_orders = itertools.groupby( - unpaid_orders['monthly_or_less'], - lambda o: o.billing_address) - - for addr, bill_orders in per_address_orders: - next_monthly_bill = Bill.objects.create(owner=user, - creation_date=creation_date, - starting_date=starting_date, # FIXME: this is a hack! - ending_date=ending_date, - due_date=postpaid_due_date) - - # It is not possible to register many-to-many relationship before - # the two end-objects are saved in database. - for order in bill_orders: - order.bill.add(next_monthly_bill) - - logger.info("Generated monthly bill {} (amount: {}) for user {}." - .format(next_monthly_bill.uuid, next_monthly_bill.total, user)) - - # Add to output. - generated_bills.append(next_monthly_bill) - - # Handle yearly bills starting on working month. - if len(unpaid_orders['yearly']) > 0: - # For every starting date, generate new bill. - for next_yearly_bill_start_on in unpaid_orders['yearly']: - # No postpaid for yearly payments. - prepaid_due_date = min(creation_date, next_yearly_bill_start_on) + BILL_PAYMENT_DELAY - # Bump by one year, remove one day. - ending_date = next_yearly_bill_start_on.replace( - year=next_yearly_bill_start_on.year+1) - timedelta(days=1) - - # There should not be any bill linked to orders with different - # billing addresses. - per_address_orders = itertools.groupby( - unpaid_orders['yearly'][next_yearly_bill_start_on], - lambda o: o.billing_address) - - for addr, bill_orders in per_address_orders: - next_yearly_bill = Bill.objects.create(owner=user, - creation_date=creation_date, - starting_date=next_yearly_bill_start_on, - ending_date=ending_date, - due_date=prepaid_due_date) - - # It is not possible to register many-to-many relationship before - # the two end-objects are saved in database. - for order in bill_orders: - order.bill.add(next_yearly_bill) - - logger.info("Generated yearly bill {} (amount: {}) for user {}." - .format(next_yearly_bill.uuid, next_yearly_bill.total, user)) - - # Add to output. - generated_bills.append(next_yearly_bill) - - # Return generated (monthly + yearly) bills. - return generated_bills - - @staticmethod - def get_unpaid_for(user): - balance = get_balance_for_user(user) - unpaid_bills = [] - # No unpaid bill if balance is positive. - if balance >= 0: - return unpaid_bills - else: - bills = Bill.objects.filter( - owner=user, - ).order_by('-creation_date') - - # Amount to be paid by the customer. - unpaid_balance = abs(balance) - for bill in bills: - if unpaid_balance <= 0: - break - - unpaid_balance -= bill.total - unpaid_bills.append(bill) - - return unpaid_bills - - @staticmethod - def get_overdue_for(user): - unpaid_bills = Bill.get_unpaid_for(user) - return list(filter(lambda bill: bill.due_date > timezone.now(), unpaid_bills)) - -class BillRecord(): - """ - Entry of a bill, dynamically generated from an order. - """ - - def __init__(self, bill, order): - self.bill = bill - self.order = order - self.recurring_price = order.recurring_price - self.recurring_period = order.recurring_period - self.description = order.description - - if self.order.starting_date >= self.bill.starting_date: - self.one_time_price = order.one_time_price - else: - self.one_time_price = 0 - - # Set decimal context for amount computations. - # XXX: understand why we need +1 here. - decimal.getcontext().prec = AMOUNT_DECIMALS + 1 - - @property - def recurring_count(self): - # Compute billing delta. - billed_until = self.bill.ending_date - if self.order.ending_date != None and self.order.ending_date <= self.bill.ending_date: - billed_until = self.order.ending_date - - billed_from = self.bill.starting_date - if self.order.starting_date > self.bill.starting_date: - billed_from = self.order.starting_date - - if billed_from > billed_until: - # TODO: think about and check edge cases. This should not be - # possible. - raise Exception('Impossible billing delta!') - - billed_delta = billed_until - billed_from - - # TODO: refactor this thing? - # TODO: weekly - # if self.recurring_period == RecurringPeriod.PER_YEAR: - # # XXX: Should always be one => we do not bill for more than one year. - # # TODO: check billed_delta is ~365 days. - # return 1 - # elif self.recurring_period == RecurringPeriod.PER_MONTH: - # days = ceil(billed_delta / timedelta(days=1)) - - # # Monthly bills always cover one single month. - # if (self.bill.starting_date.year != self.bill.starting_date.year or - # self.bill.starting_date.month != self.bill.ending_date.month): - # raise Exception('Bill {} covers more than one month. Cannot bill PER_MONTH.'. - # format(self.bill.uuid)) - - # # XXX: minumal length of monthly order is to be enforced somewhere else. - # (_, days_in_month) = monthrange( - # self.bill.starting_date.year, - # self.bill.starting_date.month) - # return round(days / days_in_month, AMOUNT_DECIMALS) - if self.recurring_period == RecurringPeriod.PER_WEEK: - weeks = ceil(billed_delta / timedelta(week=1)) - return weeks - elif self.recurring_period == RecurringPeriod.PER_DAY: - days = ceil(billed_delta / timedelta(days=1)) - return days - elif self.recurring_period == RecurringPeriod.PER_HOUR: - hours = ceil(billed_delta / timedelta(hours=1)) - return hours - elif self.recurring_period == RecurringPeriod.PER_SECOND: - seconds = ceil(billed_delta / timedelta(seconds=1)) - return seconds - elif self.recurring_period == RecurringPeriod.ONE_TIME: - return 0 - else: - raise Exception('Unsupported recurring period: {}.'. - format(self.order.recurring_period)) - - @property - def vat_rate(self): - return Decimal(VATRate.get_for_country(self.bill.billing_address.country)) - - @property - def vat_amount(self): - return self.amount * self.vat_rate - - @property - def amount(self): - return Decimal(float(self.recurring_price) * self.recurring_count) + self.one_time_price - - @property - def total(self): - return self.amount + self.vat_amount - -### -# Orders. - -# Order are assumed IMMUTABLE and used as SOURCE OF TRUST for generating -# bills. Do **NOT** mutate then! -class Order(models.Model): - uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - owner = models.ForeignKey(get_user_model(), - on_delete=models.CASCADE, - editable=False) - billing_address = models.ForeignKey(BillingAddress, on_delete=models.CASCADE) - description = models.TextField() - replaced_by = models.ForeignKey('self', on_delete=models.CASCADE, blank=True, null=True) - - # TODO: enforce ending_date - starting_date to be larger than recurring_period. - creation_date = models.DateTimeField(auto_now_add=True) - starting_date = models.DateTimeField(default=timezone.now) - ending_date = models.DateTimeField(blank=True, - null=True) - - bill = models.ManyToManyField(Bill, - editable=False, - blank=True) - - recurring_period = models.IntegerField(choices = RecurringPeriod.choices, default = RecurringPeriod.PER_30D) - - one_time_price = models.DecimalField(default=0.0, - max_digits=AMOUNT_MAX_DIGITS, - decimal_places=AMOUNT_DECIMALS, - validators=[MinValueValidator(0)]) - - recurring_price = models.DecimalField(default=0.0, - max_digits=AMOUNT_MAX_DIGITS, - decimal_places=AMOUNT_DECIMALS, - validators=[MinValueValidator(0)]) - - replaced_by = models.ForeignKey('self', - related_name='supersede', - on_delete=models.PROTECT, - blank=True, - null=True) - - depends_on = models.ForeignKey('self', - related_name='parent_of', - on_delete=models.PROTECT, - blank=True, - null=True) - - def active_before(self, ending_date): - # Was this order started before the specified ending date? - if self.starting_date <= ending_date: - if self.ending_date: - if self.ending_date > ending_date: - pass - - @property - def is_recurring(self): - return not self.recurring_period == RecurringPeriod.ONE_TIME - - @property - def is_terminated(self): - return self.ending_date != None and self.ending_date < timezone.now() - - def is_terminated_at(self, a_date): - return self.ending_date != None and self.ending_date < timezone.now() - - def terminate(self): - if not self.is_terminated: - self.ending_date = timezone.now() - self.save() - - def is_to_be_charged_in(year, month): - pass - - # Trigger initial bill generation at order creation. - def save(self, *args, **kwargs): - if self.ending_date and self.ending_date < self.starting_date: - raise ValidationError("End date cannot be before starting date") - - super().save(*args, **kwargs) - - def generate_initial_bill(self): - return Bill.generate_for(self.starting_date.year, self.starting_date.month, self.owner) - - # Used by uncloud_pay tests. - @property - def bills(self): - return Bill.objects.filter(order=self) - - @staticmethod - def from_product(product, **kwargs): - # FIXME: this is only a workaround. - billing_address = BillingAddress.get_preferred_address_for(product.owner) - if billing_address == None: - raise Exception("Owner does not have a billing address!") - - return Order(description=product.description, - one_time_price=product.one_time_price, - recurring_price=product.recurring_price, - billing_address=billing_address, - owner=product.owner, - **kwargs) - - def __str__(self): - return "Order {} created at {}, {}->{}, recurring period {}. One time price {}, recurring price {}".format( - self.uuid, self.creation_date, - self.starting_date, self.ending_date, - self.recurring_period, - self.one_time_price, - self.recurring_price) - -class OrderTimothee(models.Model): - uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - owner = models.ForeignKey(get_user_model(), - on_delete=models.CASCADE, - editable=False) - billing_address = models.ForeignKey(BillingAddress, on_delete=models.CASCADE) - - # TODO: enforce ending_date - starting_date to be larger than recurring_period. - creation_date = models.DateTimeField(auto_now_add=True) - starting_date = models.DateTimeField(default=timezone.now) - ending_date = models.DateTimeField(blank=True, - null=True) - - bill = models.ManyToManyField(Bill, - editable=False, - blank=True) - - recurring_period = models.IntegerField(choices = RecurringPeriod.choices, - default = RecurringPeriod.PER_30D) - - # Trigger initial bill generation at order creation. - def save(self, *args, **kwargs): - if self.ending_date and self.ending_date < self.starting_date: - raise ValidationError("End date cannot be before starting date") - - super().save(*args, **kwargs) - - Bill.generate_for(self.starting_date.year, self.starting_date.month, self.owner) - - @property - def records(self): - return OrderRecord.objects.filter(order=self) - - @property - def one_time_price(self): - return reduce(lambda acc, record: acc + record.one_time_price, self.records, 0) - - @property - def recurring_price(self): - return reduce(lambda acc, record: acc + record.recurring_price, self.records, 0) - - # Used by uncloud_pay tests. - @property - def bills(self): - return Bill.objects.filter(order=self) - - def add_record(self, one_time_price, recurring_price, description): - OrderRecord.objects.create(order=self, - one_time_price=one_time_price, - recurring_price=recurring_price, - description=description) - - def __str__(self): - return "Order {} created at {}, {}->{}, recurring period {}. Price one time {}, recurring {}".format( - self.uuid, self.creation_date, - self.starting_date, self.ending_date, - self.recurring_period, - self.one_time_price, - self.recurring_price) - - - -class OrderRecord(models.Model): - """ - Order records store billing informations for products: the actual product - might be mutated and/or moved to another order but we do not want to loose - the details of old orders. - - Used as source of trust to dynamically generate bill entries. - """ - - order = models.ForeignKey(Order, on_delete=models.CASCADE) - one_time_price = models.DecimalField(default=0.0, - max_digits=AMOUNT_MAX_DIGITS, - decimal_places=AMOUNT_DECIMALS, - validators=[MinValueValidator(0)]) - recurring_price = models.DecimalField(default=0.0, - max_digits=AMOUNT_MAX_DIGITS, - decimal_places=AMOUNT_DECIMALS, - validators=[MinValueValidator(0)]) - - description = models.TextField() - - - @property - def recurring_period(self): - return self.order.recurring_period - - @property - def starting_date(self): - return self.order.starting_date - - @property - def ending_date(self): - return self.order.ending_date - - -### -# Products - -# Abstract (= no database representation) class used as parent for products -# (e.g. uncloud_vm.models.VMProduct). -class Product(UncloudModel): - uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - owner = models.ForeignKey(get_user_model(), - on_delete=models.CASCADE, - editable=False) - - description = "Generic Product" - - status = models.CharField(max_length=32, - choices=UncloudStatus.choices, - default=UncloudStatus.AWAITING_PAYMENT) - - order = models.ForeignKey(Order, - on_delete=models.CASCADE, - editable=False, - null=True) - - # Default period for all products - default_recurring_period = RecurringPeriod.PER_30D - - # Used to save records. - def save(self, *args, **kwargs): - # _state.adding is switched to false after super(...) call. - being_created = self._state.adding - - # First time saving - create an order - if not self.order: - billing_address = BillingAddress.get_preferred_address_for(self.owner) - print(billing_address) - - if not billing_address: - raise ValidationError("Cannot order without a billing address") - - # FIXME: allow user to choose recurring_period - self.order = Order.objects.create(owner=self.owner, - billing_address=billing_address, - one_time_price=self.one_time_price, - recurring_period=self.default_recurring_period, - recurring_price=self.recurring_price) - - super().save(*args, **kwargs) - - # # Make sure we only create records on creation. - # if being_created: - # record = OrderRecord( - # one_time_price=self.one_time_price, - # recurring_price=self.recurring_price, - # description=self.description) - # self.order.orderrecord_set.add(record, bulk=False) - - @property - def recurring_price(self): - pass # To be implemented in child. - - @property - def one_time_price(self): - return 0 - - @property - def billing_address(self): - return self.order.billing_address - - @staticmethod - def allowed_recurring_periods(): - return RecurringPeriod.choices - - class Meta: - abstract = True - - def discounted_price_by_period(self, requested_period): - """ - Each product has a standard recurring period for which - we define a pricing. I.e. VPN is usually year, VM is usually monthly. - - The user can opt-in to use a different period, which influences the price: - The longer a user commits, the higher the discount. - - Products can also be limited in the available periods. For instance - a VPN only makes sense to be bought for at least one day. - - Rules are as follows: - - given a standard recurring period of ..., changing to ... modifies price ... - - - # One month for free if buying / year, compared to a month: about 8.33% discount - per_year -> per_month -> /11 - per_month -> per_year -> *11 - - # Month has 30.42 days on average. About 7.9% discount to go monthly - per_month -> per_day -> /28 - per_day -> per_month -> *28 - - # Day has 24h, give one for free - per_day -> per_hour -> /23 - per_hour -> per_day -> /23 - - - Examples - - VPN @ 120CHF/y becomes - - 10.91 CHF/month (130.91 CHF/year) - - 0.39 CHF/day (142.21 CHF/year) - - VM @ 15 CHF/month becomes - - 165 CHF/month (13.75 CHF/month) - - 0.54 CHF/day (16.30 CHF/month) - - """ - - - if self.default_recurring_period == RecurringPeriod.PER_365D: - if requested_period == RecurringPeriod.PER_365D: - return self.recurring_price - if requested_period == RecurringPeriod.PER_30D: - return self.recurring_price/11. - if requested_period == RecurringPeriod.PER_DAY: - return self.recurring_price/11./28. - - elif self.default_recurring_period == RecurringPeriod.PER_30D: - if requested_period == RecurringPeriod.PER_365D: - return self.recurring_price*11 - if requested_period == RecurringPeriod.PER_30D: - return self.recurring_price - if requested_period == RecurringPeriod.PER_DAY: - return self.recurring_price/28. - - elif self.default_recurring_period == RecurringPeriod.PER_DAY: - if requested_period == RecurringPeriod.PER_365D: - return self.recurring_price*11*28 - if requested_period == RecurringPeriod.PER_30D: - return self.recurring_price*28 - if requested_period == RecurringPeriod.PER_DAY: - return self.recurring_price - else: - # FIXME: use the right type of exception here! - raise Exception("Did not implement the discounter for this case") diff --git a/uncloud_pay/tests-before-refactor.py b/uncloud_pay/tests-before-refactor.py deleted file mode 100644 index 49c51c6..0000000 --- a/uncloud_pay/tests-before-refactor.py +++ /dev/null @@ -1,721 +0,0 @@ -from django.test import TestCase -from django.contrib.auth import get_user_model -from datetime import datetime, date, timedelta -from django.utils import timezone - -from .models import * -from uncloud_service.models import GenericServiceProduct - -class OrderTestCase(TestCase): - """ - The heart of ordering products - """ - - def setUp(self): - self.user = get_user_model().objects.create( - username='random_user', - email='jane.random@domain.tld') - - self.ba = BillingAddress.objects.create( - owner=self.user, - organization = 'Test org', - street="unknown", - city="unknown", - postal_code="somewhere else", - active=True) - - def test_create_one_time_product(self): - """ - One time payment products cannot be updated - can they? - """ - - p = SampleOneTimeProduct.objects.create(owner=self.user) - - self.assertEqual(p.one_time_price, 5) - self.assertEqual(p.recurring_price, 0) - - -# class ProductTestCase(TestCase): -# """ -# Test products and products <-> order interaction -# """ - -# def setUp(self): -# self.user = get_user_model().objects.create( -# username='random_user', -# email='jane.random@domain.tld') - -# self.ba = BillingAddress.objects.create( -# owner=self.user, -# organization = 'Test org', -# street="unknown", -# city="unknown", -# postal_code="somewhere else", -# active=True) - -# def test_create_one_time_product(self): -# """ -# One time payment products cannot be updated - can they? -# """ - -# p = SampleOneTimeProduct.objects.create(owner=self.user) - -# self.assertEqual(p.one_time_price, 5) -# self.assertEqual(p.recurring_price, 0) - -# def test_create_product_without_active_billing_address(self): -# """ -# Fail to create a product without an active billing address -# """ - -# self.ba.active = False -# self.ba.save() - -# with self.assertRaises(ValidationError): -# p = SampleOneTimeProduct.objects.create(owner=self.user) - -# def test_create_product_without_billing_address(self): -# """ -# Fail to create a product without a billing address -# """ - -# user2 = get_user_model().objects.create( -# username='random_user2', -# email='jane.randomly@domain.tld') - -# with self.assertRaises(ValidationError): -# p = SampleOneTimeProduct.objects.create(owner=user2) - - -# def test_create_order_creates_correct_order_count(self): -# """ -# Ensure creating orders from product only creates 1 order -# """ - -# # One order -# p = SampleOneTimeProduct.objects.create(owner=self.user) -# p.create_order(timezone.make_aware(datetime.datetime(2020,3,3))) - -# order_count = Order.objects.filter(owner=self.user).count() -# self.assertEqual(order_count, 1) - -# # One more order -# p = SampleRecurringProduct.objects.create(owner=self.user) -# p.create_order(timezone.make_aware(datetime.datetime(2020,3,3))) - -# order_count = Order.objects.filter(owner=self.user).count() -# self.assertEqual(order_count, 2) - -# # Should create 2 orders -# p = SampleRecurringProductOneTimeFee.objects.create(owner=self.user) -# p.create_order(timezone.make_aware(datetime.datetime(2020,3,3))) - -# order_count = Order.objects.filter(owner=self.user).count() -# self.assertEqual(order_count, 4) - - - # def test_update_recurring_order(self): - # """ - # Ensure creating orders from product only creates 1 order - # """ - - # p = SampleRecurringProduct.objects.create(owner=self.user) - # p.create_order(timezone.make_aware(datetime.datetime(2020,3,3))) - - # p.create_or_update_recurring_order(timezone.make_aware(datetime.datetime(2020,3,4))) - - # # FIXME: where is the assert? - - -class BillingAddressTestCase(TestCase): - def setUp(self): - self.user = get_user_model().objects.create( - username='random_user', - email='jane.random@domain.tld') - - - def test_user_no_address(self): - """ - Raise an error, when there is no address - """ - - self.assertRaises(uncloud_pay.models.BillingAddress.DoesNotExist, - BillingAddress.get_address_for, - self.user) - - def test_user_only_inactive_address(self): - """ - Raise an error, when there is no active address - """ - - ba = BillingAddress.objects.create( - owner=self.user, - organization = 'Test org', - street="unknown", - city="unknown", - postal_code="somewhere else", - active=False) - - self.assertRaises(uncloud_pay.models.BillingAddress.DoesNotExist, - BillingAddress.get_address_for, - self.user) - - def test_find_active_address(self): - """ - Find the active address - """ - - ba = BillingAddress.objects.create( - owner=self.user, - organization = 'Test org', - street="unknown", - city="unknown", - postal_code="unknown", - active=True) - - - self.assertEqual(BillingAddress.get_address_for(self.user), ba) - - def test_find_right_address_with_multiple_addresses(self): - """ - Find the active address only, skip inactive - """ - - ba = BillingAddress.objects.create( - owner=self.user, - organization = 'Test org', - street="unknown", - city="unknown", - postal_code="unknown", - active=True) - - ba2 = BillingAddress.objects.create( - owner=self.user, - organization = 'Test org', - street="unknown", - city="unknown", - postal_code="somewhere else", - active=False) - - - self.assertEqual(BillingAddress.get_address_for(self.user), ba) - - def test_change_addresses(self): - """ - Switch the active address - """ - - ba = BillingAddress.objects.create( - owner=self.user, - organization = 'Test org', - street="unknown", - city="unknown", - postal_code="unknown", - active=True) - - self.assertEqual(BillingAddress.get_address_for(self.user), ba) - - ba.active=False - ba.save() - - ba2 = BillingAddress.objects.create( - owner=self.user, - organization = 'Test org', - street="unknown", - city="unknown", - postal_code="somewhere else", - active=True) - - self.assertEqual(BillingAddress.get_address_for(self.user), ba2) - - - -class BillTestCase(TestCase): - def setUp(self): - self.user_without_address = get_user_model().objects.create( - username='no_home_person', - email='far.away@domain.tld') - - self.user = get_user_model().objects.create( - username='jdoe', - email='john.doe@domain.tld') - - self.recurring_user = get_user_model().objects.create( - username='recurrent_product_user', - email='jane.doe@domain.tld') - - self.user_addr = BillingAddress.objects.create( - owner=self.user, - organization = 'Test org', - street="unknown", - city="unknown", - postal_code="unknown", - active=True) - - self.recurring_user_addr = BillingAddress.objects.create( - owner=self.recurring_user, - organization = 'Test org', - street="Somewhere", - city="Else", - postal_code="unknown", - active=True) - - self.order_meta = {} - self.order_meta[1] = { - 'starting_date': timezone.make_aware(datetime.datetime(2020,3,3)), - 'ending_date': timezone.make_aware(datetime.datetime(2020,4,17)), - 'price': 15, - 'description': 'One chocolate bar' - } - - self.one_time_order = Order.objects.create( - owner=self.user, - starting_date=self.order_meta[1]['starting_date'], - ending_date=self.order_meta[1]['ending_date'], - recurring_period=RecurringPeriod.ONE_TIME, - price=self.order_meta[1]['price'], - description=self.order_meta[1]['description'], - billing_address=BillingAddress.get_address_for(self.user)) - - self.recurring_order = Order.objects.create( - owner=self.recurring_user, - starting_date=timezone.make_aware(datetime.datetime(2020,3,3)), - recurring_period=RecurringPeriod.PER_30D, - price=15, - description="A pretty VM", - billing_address=BillingAddress.get_address_for(self.recurring_user) - ) - - # used for generating multiple bills - self.bill_dates = [ - timezone.make_aware(datetime.datetime(2020,3,31)), - timezone.make_aware(datetime.datetime(2020,4,30)), - timezone.make_aware(datetime.datetime(2020,5,31)), - ] - - - - def test_bill_one_time_one_bill_record(self): - """ - Ensure there is only 1 bill record per order - """ - - bill = Bill.create_next_bill_for_user_address(self.user_addr) - - self.assertEqual(self.one_time_order.billrecord_set.count(), 1) - - def test_bill_sum_onetime(self): - """ - Check the bill sum for a single one time order - """ - - bill = Bill.create_next_bill_for_user_address(self.user_addr) - self.assertEqual(bill.sum, self.order_meta[1]['price']) - - - def test_bill_creates_record_for_recurring_order(self): - """ - Ensure there is only 1 bill record per order - """ - - bill = Bill.create_next_bill_for_user_address(self.recurring_user_addr) - - self.assertEqual(self.recurring_order.billrecord_set.count(), 1) - self.assertEqual(bill.billrecord_set.count(), 1) - - - def test_new_bill_after_closing(self): - """ - After closing a bill and the user has a recurring product, - the next bill run should create e new bill - """ - - for ending_date in self.bill_dates: - b = Bill.create_next_bill_for_user_address(self.recurring_user_addr, ending_date) - b.close() - - bill_count = Bill.objects.filter(owner=self.recurring_user).count() - - self.assertEqual(len(self.bill_dates), bill_count) - - def test_multi_addr_multi_bill(self): - """ - Ensure multiple bills are created if orders exist with different billing addresses - """ - - username="lotsofplaces" - multi_addr_user = get_user_model().objects.create( - username=username, - email=f"{username}@example.org") - - user_addr1 = BillingAddress.objects.create( - owner=multi_addr_user, - organization = 'Test org', - street="unknown", - city="unknown", - postal_code="unknown", - active=True) - - order1 = Order.objects.create( - owner=multi_addr_user, - starting_date=self.order_meta[1]['starting_date'], - ending_date=self.order_meta[1]['ending_date'], - recurring_period=RecurringPeriod.ONE_TIME, - price=self.order_meta[1]['price'], - description=self.order_meta[1]['description'], - billing_address=BillingAddress.get_address_for(self.user)) - - # Make this address inactive - user_addr1.active = False - user_addr1.save() - - user_addr2 = BillingAddress.objects.create( - owner=multi_addr_user, - organization = 'Test2 org', - street="unknown2", - city="unknown2", - postal_code="unknown2", - active=True) - - order2 = Order.objects.create( - owner=multi_addr_user, - starting_date=self.order_meta[1]['starting_date'], - ending_date=self.order_meta[1]['ending_date'], - recurring_period=RecurringPeriod.ONE_TIME, - price=self.order_meta[1]['price'], - description=self.order_meta[1]['description'], - billing_address=BillingAddress.get_address_for(self.user)) - - - bills = Bill.create_next_bills_for_user(multi_addr_user) - - self.assertEqual(len(bills), 2) - - - # TO BE IMPLEMENTED -- once orders can be marked as "done" / "inactive" / "not for billing" - # def test_skip_disabled_orders(self): - # """ - # Ensure that a bill only considers "active" orders - # """ - - # self.assertEqual(1, 2) - - -class ModifyProductTestCase(TestCase): - def setUp(self): - self.user = get_user_model().objects.create( - username='random_user', - email='jane.random@domain.tld') - - self.ba = BillingAddress.objects.create( - owner=self.user, - organization = 'Test org', - street="unknown", - city="unknown", - postal_code="somewhere else", - active=True) - - def test_no_pro_rata_first_bill(self): - """ - The bill should NOT contain a partial amount -- this is a BILL TEST :-) - """ - - price = 5 - - # Standard 30d recurring product - product = SampleRecurringProduct.objects.create(owner=self.user, - rc_price=price) - - starting_date = timezone.make_aware(datetime.datetime(2020,3,3)) - ending_date = timezone.make_aware(datetime.datetime(2020,3,31)) - time_diff = (ending_date - starting_date).total_seconds() - - product.create_order(starting_date) - - bills = Bill.create_next_bills_for_user(self.user, - ending_date=ending_date) - - - # We expect 1 bill for 1 billing address and 1 time frame - self.assertEqual(len(bills), 1) - - pro_rata_amount = time_diff / product.default_recurring_period.value - - self.assertNotEqual(bills[0].sum, pro_rata_amount * price) - self.assertEqual(bills[0].sum, price) - - - def test_downgrade_product(self): - """ - Test downgrading behaviour: - - We create a recurring product (recurring time: 30 days) and downgrade after 15 days. - - We create the bill right AFTER the end of the first order. - - Expected result: - - - First bill record for 30 days - - Second bill record starting after 30 days - - Bill contains two bill records - - """ - - user = self.user - - - - starting_price = 10 - downgrade_price = 5 - - - starting_date = timezone.make_aware(datetime.datetime(2019,3,3)) - first_order_should_end_at = starting_date + datetime.timedelta(days=30) - change1_date = start_after(starting_date + datetime.timedelta(days=15)) - bill_ending_date = change1_date + datetime.timedelta(days=1) - - - product = SampleRecurringProduct.objects.create(owner=user, rc_price=starting_price) - product.create_order(starting_date) - - product.rc_price = downgrade_price - product.save() - product.create_or_update_recurring_order(when_to_start=change1_date) - - bills = Bill.create_next_bills_for_user(user, ending_date=bill_ending_date) - - bill = bills[0] - bill_records = BillRecord.objects.filter(bill=bill) - - self.assertEqual(len(bill_records), 2) - - self.assertEqual(bill_records[0].starting_date, starting_date) - - self.assertEqual(bill_records[0].order.ending_date, first_order_should_end_at) - - # self.assertEqual(bill_records[0].ending_date, first_order_should_end_at) - - # self.assertEqual(bill_records[0].quantity, 1) - - # self.assertEqual(bill_records[1].quantity, 1) - # self.assertEqual(int(bill.sum), 15) - - def test_upgrade_product(self): - """ - Test upgrading behaviour - """ - - user = self.user - - # Create product - starting_date = timezone.make_aware(datetime.datetime(2019,3,3)) - starting_price = 10 - product = SampleRecurringProduct.objects.create(owner=user, rc_price=starting_price) - product.create_order(starting_date) - - change1_date = start_after(starting_date + datetime.timedelta(days=15)) - product.rc_price = 20 - product.save() - product.create_or_update_recurring_order(when_to_start=change1_date) - - bill_ending_date = change1_date + datetime.timedelta(days=1) - bills = Bill.create_next_bills_for_user(user, ending_date=bill_ending_date) - - bill = bills[0] - bill_records = BillRecord.objects.filter(bill=bill) - - self.assertEqual(len(bill_records), 2) - self.assertEqual(bill_records[0].quantity, .5) - - self.assertEqual(bill_records[0].ending_date, end_before(change1_date)) - - self.assertEqual(bill_records[1].quantity, 1) - self.assertEqual(bill_records[1].starting_date, change1_date) - - self.assertEqual(int(bill.sum), 25) - - - - # def test_bill_for_increasing_product(self): - # """ - # Modify a product, see one pro rata entry - # """ - - # # Create product - # starting_date = timezone.make_aware(datetime.datetime(2019,3,3)) - # starting_price = 30.5 - # product = SampleRecurringProduct.objects.create(owner=self.user, - # rc_price=starting_price) - # product.create_order(starting_date) - - # recurring_period = product.default_recurring_period.value - - # # First change - # change1_date = timezone.make_aware(datetime.datetime(2019,4,17)) - # product.rc_price = 49.5 - # product.save() - # product.create_or_update_recurring_order(when_to_start=change1_date) - - # # Second change - # change2_date = timezone.make_aware(datetime.datetime(2019,5,8)) - # product.rc_price = 56.5 - # product.save() - # product.create_or_update_recurring_order(when_to_start=change2_date) - - # # Create bill one month after 2nd change - # bill_ending_date = timezone.make_aware(datetime.datetime(2019,6,30)) - # bills = Bill.create_next_bills_for_user(self.user, - # ending_date=bill_ending_date) - - # # only one bill in this test case - # bill = bills[0] - - # expected_amount = starting_price - - # d2 = starting_date + recurring_period - # duration2 = change1_date - d2 - - # expected_amount = 0 - - # # Expected bill sum & records: - # # 2019-03-03 - 2019-04-02 +30d: 30.5 - # # 2019-04-02 - 2019-04-17: +15d: 15.25 - # # 2019-04-17 - 2019-05-08: +21d: (21/30) * 49.5 - # # 2019-05-08 - 2019-06-07: +30d: 56.5 - # # 2019-06-07 - 2019-07-07: +30d: 56.5 - - - # self.assertEqual(bills[0].sum, price) - - # # expeted result: - # # 1x 5 chf bill record - # # 1x 5 chf bill record - # # 1x 10 partial bill record - - - -# class NotABillingTC(TestCase): -# #class BillingTestCase(TestCase): -# def setUp(self): -# self.user = get_user_model().objects.create( -# username='jdoe', -# email='john.doe@domain.tld') -# self.billing_address = BillingAddress.objects.create( -# owner=self.user, -# street="unknown", -# city="unknown", -# postal_code="unknown") - -# def test_basic_monthly_billing(self): -# one_time_price = 10 -# recurring_price = 20 -# description = "Test Product 1" - -# # Three months: full, full, partial. -# # starting_date = datetime.fromisoformat('2020-03-01') -# starting_date = datetime(2020,3,1) -# ending_date = datetime(2020,5,8) - -# # Create order to be billed. -# order = Order.objects.create( -# owner=self.user, -# starting_date=starting_date, -# ending_date=ending_date, -# recurring_period=RecurringPeriod.PER_30D, -# recurring_price=recurring_price, -# one_time_price=one_time_price, -# description=description, -# billing_address=self.billing_address) - -# # Generate & check bill for first month: full recurring_price + setup. -# first_month_bills = order.generate_initial_bill() -# self.assertEqual(len(first_month_bills), 1) -# self.assertEqual(first_month_bills[0].amount, one_time_price + recurring_price) - -# # Generate & check bill for second month: full recurring_price. -# second_month_bills = Bill.generate_for(2020, 4, self.user) -# self.assertEqual(len(second_month_bills), 1) -# self.assertEqual(second_month_bills[0].amount, recurring_price) - -# # Generate & check bill for third and last month: partial recurring_price. -# third_month_bills = Bill.generate_for(2020, 5, self.user) -# self.assertEqual(len(third_month_bills), 1) -# # 31 days in May. -# self.assertEqual(float(third_month_bills[0].amount), -# round(round((7/31), AMOUNT_DECIMALS) * recurring_price, AMOUNT_DECIMALS)) - -# # Check that running Bill.generate_for() twice does not create duplicates. -# self.assertEqual(len(Bill.generate_for(2020, 3, self.user)), 0) - -# def test_basic_yearly_billing(self): -# one_time_price = 10 -# recurring_price = 150 -# description = "Test Product 1" - -# starting_date = datetime.fromisoformat('2020-03-31T08:05:23') - -# # Create order to be billed. -# order = Order.objects.create( -# owner=self.user, -# starting_date=starting_date, -# recurring_period=RecurringPeriod.PER_365D, -# recurring_price=recurring_price, -# one_time_price=one_time_price, -# description=description, -# billing_address=self.billing_address) - -# # Generate & check bill for first year: recurring_price + setup. -# first_year_bills = order.generate_initial_bill() -# self.assertEqual(len(first_year_bills), 1) -# self.assertEqual(first_year_bills[0].starting_date.date(), -# date.fromisoformat('2020-03-31')) -# self.assertEqual(first_year_bills[0].ending_date.date(), -# date.fromisoformat('2021-03-30')) -# self.assertEqual(first_year_bills[0].amount, -# recurring_price + one_time_price) - -# # Generate & check bill for second year: recurring_price. -# second_year_bills = Bill.generate_for(2021, 3, self.user) -# self.assertEqual(len(second_year_bills), 1) -# self.assertEqual(second_year_bills[0].starting_date.date(), -# date.fromisoformat('2021-03-31')) -# self.assertEqual(second_year_bills[0].ending_date.date(), -# date.fromisoformat('2022-03-30')) -# self.assertEqual(second_year_bills[0].amount, recurring_price) - -# # Check that running Bill.generate_for() twice does not create duplicates. -# self.assertEqual(len(Bill.generate_for(2020, 3, self.user)), 0) -# self.assertEqual(len(Bill.generate_for(2020, 4, self.user)), 0) -# self.assertEqual(len(Bill.generate_for(2020, 2, self.user)), 0) -# self.assertEqual(len(Bill.generate_for(2021, 3, self.user)), 0) - -# def test_basic_hourly_billing(self): -# one_time_price = 10 -# recurring_price = 1.4 -# description = "Test Product 1" - -# starting_date = datetime.fromisoformat('2020-03-31T08:05:23') -# ending_date = datetime.fromisoformat('2020-04-01T11:13:32') - -# # Create order to be billed. -# order = Order.objects.create( -# owner=self.user, -# starting_date=starting_date, -# ending_date=ending_date, -# recurring_period=RecurringPeriod.PER_HOUR, -# recurring_price=recurring_price, -# one_time_price=one_time_price, -# description=description, -# billing_address=self.billing_address) - -# # Generate & check bill for first month: recurring_price + setup. -# first_month_bills = order.generate_initial_bill() -# self.assertEqual(len(first_month_bills), 1) -# self.assertEqual(float(first_month_bills[0].amount), -# round(16 * recurring_price, AMOUNT_DECIMALS) + one_time_price) - -# # Generate & check bill for first month: recurring_price. -# second_month_bills = Bill.generate_for(2020, 4, self.user) -# self.assertEqual(len(second_month_bills), 1) -# self.assertEqual(float(second_month_bills[0].amount), -# round(12 * recurring_price, AMOUNT_DECIMALS)) diff --git a/uncloud_pay/tests.py b/uncloud_pay/tests.py index 0ebd11c..ca91cc9 100644 --- a/uncloud_pay/tests.py +++ b/uncloud_pay/tests.py @@ -244,36 +244,6 @@ class ModifyOrderTestCase(TestCase): -# def test_no_pro_rata_first_bill(self): -# """ -# The bill should NOT contain a partial amount -- this is a BILL TEST :-) -# """ - -# price = 5 - -# # Standard 30d recurring product -# product = SampleRecurringProduct.objects.create(owner=self.user, -# rc_price=price) - -# starting_date = timezone.make_aware(datetime.datetime(2020,3,3)) -# ending_date = timezone.make_aware(datetime.datetime(2020,3,31)) -# time_diff = (ending_date - starting_date).total_seconds() - -# product.create_order(starting_date) - -# bills = Bill.create_next_bills_for_user(self.user, -# ending_date=ending_date) - - -# # We expect 1 bill for 1 billing address and 1 time frame -# self.assertEqual(len(bills), 1) - -# pro_rata_amount = time_diff / product.default_recurring_period.value - -# self.assertNotEqual(bills[0].sum, pro_rata_amount * price) -# self.assertEqual(bills[0].sum, price) - - def test_downgrade_product(self): """ Test downgrading behaviour: @@ -300,7 +270,6 @@ class ModifyOrderTestCase(TestCase): change1_date = start_after(starting_date + datetime.timedelta(days=15)) bill_ending_date = change1_date + datetime.timedelta(days=1) - order1 = Order.objects.create(owner=self.user, billing_address=BillingAddress.get_address_for(self.user), product=self.product, @@ -309,12 +278,6 @@ class ModifyOrderTestCase(TestCase): order1.update_order(vm_order_downgrade_config, starting_date=change1_date) - # product = Product.objects.create(owner=user, rc_price=starting_price) - # product.create_order(starting_date) - # product.rc_price = downgrade_price - # product.save() - # product.create_or_update_recurring_order(when_to_start=change1_date) - bills = Bill.create_next_bills_for_user(user, ending_date=bill_ending_date) bill = bills[0] @@ -325,105 +288,6 @@ class ModifyOrderTestCase(TestCase): self.assertEqual(bill_records[0].starting_date, starting_date) self.assertEqual(bill_records[0].order.ending_date, first_order_should_end_at) - # self.assertEqual(bill_records[0].ending_date, first_order_should_end_at) - - # self.assertEqual(bill_records[0].quantity, 1) - - # self.assertEqual(bill_records[1].quantity, 1) - # self.assertEqual(int(bill.sum), 15) - -# def test_upgrade_product(self): -# """ -# Test upgrading behaviour -# """ - -# user = self.user - -# # Create product -# starting_date = timezone.make_aware(datetime.datetime(2019,3,3)) -# starting_price = 10 -# product = SampleRecurringProduct.objects.create(owner=user, rc_price=starting_price) -# product.create_order(starting_date) - -# change1_date = start_after(starting_date + datetime.timedelta(days=15)) -# product.rc_price = 20 -# product.save() -# product.create_or_update_recurring_order(when_to_start=change1_date) - -# bill_ending_date = change1_date + datetime.timedelta(days=1) -# bills = Bill.create_next_bills_for_user(user, ending_date=bill_ending_date) - -# bill = bills[0] -# bill_records = BillRecord.objects.filter(bill=bill) - -# self.assertEqual(len(bill_records), 2) -# self.assertEqual(bill_records[0].quantity, .5) - -# self.assertEqual(bill_records[0].ending_date, end_before(change1_date)) - -# self.assertEqual(bill_records[1].quantity, 1) -# self.assertEqual(bill_records[1].starting_date, change1_date) - -# self.assertEqual(int(bill.sum), 25) - - - - # def test_bill_for_increasing_product(self): - # """ - # Modify a product, see one pro rata entry - # """ - - # # Create product - # starting_date = timezone.make_aware(datetime.datetime(2019,3,3)) - # starting_price = 30.5 - # product = SampleRecurringProduct.objects.create(owner=self.user, - # rc_price=starting_price) - # product.create_order(starting_date) - - # recurring_period = product.default_recurring_period.value - - # # First change - # change1_date = timezone.make_aware(datetime.datetime(2019,4,17)) - # product.rc_price = 49.5 - # product.save() - # product.create_or_update_recurring_order(when_to_start=change1_date) - - # # Second change - # change2_date = timezone.make_aware(datetime.datetime(2019,5,8)) - # product.rc_price = 56.5 - # product.save() - # product.create_or_update_recurring_order(when_to_start=change2_date) - - # # Create bill one month after 2nd change - # bill_ending_date = timezone.make_aware(datetime.datetime(2019,6,30)) - # bills = Bill.create_next_bills_for_user(self.user, - # ending_date=bill_ending_date) - - # # only one bill in this test case - # bill = bills[0] - - # expected_amount = starting_price - - # d2 = starting_date + recurring_period - # duration2 = change1_date - d2 - - # expected_amount = 0 - - # # Expected bill sum & records: - # # 2019-03-03 - 2019-04-02 +30d: 30.5 - # # 2019-04-02 - 2019-04-17: +15d: 15.25 - # # 2019-04-17 - 2019-05-08: +21d: (21/30) * 49.5 - # # 2019-05-08 - 2019-06-07: +30d: 56.5 - # # 2019-06-07 - 2019-07-07: +30d: 56.5 - - - # self.assertEqual(bills[0].sum, price) - - # # expeted result: - # # 1x 5 chf bill record - # # 1x 5 chf bill record - # # 1x 10 partial bill record - class BillTestCase(TestCase): """ @@ -431,6 +295,8 @@ class BillTestCase(TestCase): """ def setUp(self): + RecurringPeriod.populate_db_defaults() + self.user_without_address = get_user_model().objects.create( username='no_home_person', email='far.away@domain.tld') @@ -500,7 +366,7 @@ class BillTestCase(TestCase): def order_chocolate(self): return Order.objects.create( owner=self.user, - recurring_period=RecurringPeriod.ONE_TIME, + recurring_period=RecurringPeriod.objects.get(name="Onetime"), product=self.chocolate, billing_address=BillingAddress.get_address_for(self.user), starting_date=self.order_meta[1]['starting_date'], @@ -522,7 +388,7 @@ class BillTestCase(TestCase): return Order.objects.create( owner=self.user, - recurring_period=RecurringPeriod.ONE_TIME, + recurring_period=RecurringPeriod.objects.get(name="Onetime"), product=self.chocolate, billing_address=BillingAddress.get_address_for(self.user), starting_date=self.order_meta[1]['starting_date'], @@ -580,167 +446,6 @@ class BillTestCase(TestCase): self.assertEqual(len(self.bill_dates), bill_count) - # def test_multi_addr_multi_bill(self): - # """ - # Ensure multiple bills are created if orders exist with different billing addresses - # """ - - # username="lotsofplaces" - # multi_addr_user = get_user_model().objects.create( - # username=username, - # email=f"{username}@example.org") - - # user_addr1 = BillingAddress.objects.create( - # owner=multi_addr_user, - # organization = 'Test org', - # street="unknown", - # city="unknown", - # postal_code="unknown", - # active=True) - - # order1 = Order.objects.create( - # owner=multi_addr_user, - # recurring_period=RecurringPeriod.ONE_TIME, - # product=self.chocolate, - # billing_address=BillingAddress.get_address_for(self.user), - # starting_date=self.order_meta[1]['starting_date'], - # ending_date=self.order_meta[1]['ending_date'], - # config=chocolate_order_config) - - # order1 = Order.objects.create( - # owner=multi_addr_user, - # starting_date=self.order_meta[1]['starting_date'], - # ending_date=self.order_meta[1]['ending_date'], - # recurring_period=RecurringPeriod.ONE_TIME, - # price=self.order_meta[1]['price'], - # description=self.order_meta[1]['description'], - # billing_address=BillingAddress.get_address_for(self.user)) - - # # Make this address inactive - # user_addr1.active = False - # user_addr1.save() - - # user_addr2 = BillingAddress.objects.create( - # owner=multi_addr_user, - # organization = 'Test2 org', - # street="unknown2", - # city="unknown2", - # postal_code="unknown2", - # active=True) - - # order2 = Order.objects.create( - # owner=multi_addr_user, - # starting_date=self.order_meta[1]['starting_date'], - # ending_date=self.order_meta[1]['ending_date'], - # recurring_period=RecurringPeriod.ONE_TIME, - # price=self.order_meta[1]['price'], - # description=self.order_meta[1]['description'], - # billing_address=BillingAddress.get_address_for(self.user)) - - - # bills = Bill.create_next_bills_for_user(multi_addr_user) - - # self.assertEqual(len(bills), 2) - - -# # TO BE IMPLEMENTED -- once orders can be marked as "done" / "inactive" / "not for billing" -# # def test_skip_disabled_orders(self): -# # """ -# # Ensure that a bill only considers "active" orders -# # """ - -# # self.assertEqual(1, 2) - -# class ProductTestCase(TestCase): -# """ -# Test products and products <-> order interaction -# """ - -# def setUp(self): -# self.user = get_user_model().objects.create( -# username='random_user', -# email='jane.random@domain.tld') - -# self.ba = BillingAddress.objects.create( -# owner=self.user, -# organization = 'Test org', -# street="unknown", -# city="unknown", -# postal_code="somewhere else", -# active=True) - -# def test_create_one_time_product(self): -# """ -# One time payment products cannot be updated - can they? -# """ - -# p = SampleOneTimeProduct.objects.create(owner=self.user) - -# self.assertEqual(p.one_time_price, 5) -# self.assertEqual(p.recurring_price, 0) - -# def test_create_product_without_active_billing_address(self): -# """ -# Fail to create a product without an active billing address -# """ - -# self.ba.active = False -# self.ba.save() - -# with self.assertRaises(ValidationError): -# p = SampleOneTimeProduct.objects.create(owner=self.user) - -# def test_create_product_without_billing_address(self): -# """ -# Fail to create a product without a billing address -# """ - -# user2 = get_user_model().objects.create( -# username='random_user2', -# email='jane.randomly@domain.tld') - -# with self.assertRaises(ValidationError): -# p = SampleOneTimeProduct.objects.create(owner=user2) - - -# def test_create_order_creates_correct_order_count(self): -# """ -# Ensure creating orders from product only creates 1 order -# """ - -# # One order -# p = SampleOneTimeProduct.objects.create(owner=self.user) -# p.create_order(timezone.make_aware(datetime.datetime(2020,3,3))) - -# order_count = Order.objects.filter(owner=self.user).count() -# self.assertEqual(order_count, 1) - -# # One more order -# p = SampleRecurringProduct.objects.create(owner=self.user) -# p.create_order(timezone.make_aware(datetime.datetime(2020,3,3))) - -# order_count = Order.objects.filter(owner=self.user).count() -# self.assertEqual(order_count, 2) - -# # Should create 2 orders -# p = SampleRecurringProductOneTimeFee.objects.create(owner=self.user) -# p.create_order(timezone.make_aware(datetime.datetime(2020,3,3))) - -# order_count = Order.objects.filter(owner=self.user).count() -# self.assertEqual(order_count, 4) - - - # def test_update_recurring_order(self): - # """ - # Ensure creating orders from product only creates 1 order - # """ - - # p = SampleRecurringProduct.objects.create(owner=self.user) - # p.create_order(timezone.make_aware(datetime.datetime(2020,3,3))) - - # p.create_or_update_recurring_order(timezone.make_aware(datetime.datetime(2020,3,4))) - - # # FIXME: where is the assert? class BillingAddressTestCase(TestCase): @@ -758,219 +463,3 @@ class BillingAddressTestCase(TestCase): self.assertRaises(uncloud_pay.models.BillingAddress.DoesNotExist, BillingAddress.get_address_for, self.user) - -# def test_user_only_inactive_address(self): -# """ -# Raise an error, when there is no active address -# """ - -# ba = BillingAddress.objects.create( -# owner=self.user, -# organization = 'Test org', -# street="unknown", -# city="unknown", -# postal_code="somewhere else", -# active=False) - -# self.assertRaises(uncloud_pay.models.BillingAddress.DoesNotExist, -# BillingAddress.get_address_for, -# self.user) - -# def test_find_active_address(self): -# """ -# Find the active address -# """ - -# ba = BillingAddress.objects.create( -# owner=self.user, -# organization = 'Test org', -# street="unknown", -# city="unknown", -# postal_code="unknown", -# active=True) - - -# self.assertEqual(BillingAddress.get_address_for(self.user), ba) - -# def test_find_right_address_with_multiple_addresses(self): -# """ -# Find the active address only, skip inactive -# """ - -# ba = BillingAddress.objects.create( -# owner=self.user, -# organization = 'Test org', -# street="unknown", -# city="unknown", -# postal_code="unknown", -# active=True) - -# ba2 = BillingAddress.objects.create( -# owner=self.user, -# organization = 'Test org', -# street="unknown", -# city="unknown", -# postal_code="somewhere else", -# active=False) - - -# self.assertEqual(BillingAddress.get_address_for(self.user), ba) - -# def test_change_addresses(self): -# """ -# Switch the active address -# """ - -# ba = BillingAddress.objects.create( -# owner=self.user, -# organization = 'Test org', -# street="unknown", -# city="unknown", -# postal_code="unknown", -# active=True) - -# self.assertEqual(BillingAddress.get_address_for(self.user), ba) - -# ba.active=False -# ba.save() - -# ba2 = BillingAddress.objects.create( -# owner=self.user, -# organization = 'Test org', -# street="unknown", -# city="unknown", -# postal_code="somewhere else", -# active=True) - -# self.assertEqual(BillingAddress.get_address_for(self.user), ba2) - - - - - - -# class NotABillingTC(TestCase): -# #class BillingTestCase(TestCase): -# def setUp(self): -# self.user = get_user_model().objects.create( -# username='jdoe', -# email='john.doe@domain.tld') -# self.billing_address = BillingAddress.objects.create( -# owner=self.user, -# street="unknown", -# city="unknown", -# postal_code="unknown") - -# def test_basic_monthly_billing(self): -# one_time_price = 10 -# recurring_price = 20 -# description = "Test Product 1" - -# # Three months: full, full, partial. -# # starting_date = datetime.fromisoformat('2020-03-01') -# starting_date = datetime(2020,3,1) -# ending_date = datetime(2020,5,8) - -# # Create order to be billed. -# order = Order.objects.create( -# owner=self.user, -# starting_date=starting_date, -# ending_date=ending_date, -# recurring_period=RecurringPeriod.PER_30D, -# recurring_price=recurring_price, -# one_time_price=one_time_price, -# description=description, -# billing_address=self.billing_address) - -# # Generate & check bill for first month: full recurring_price + setup. -# first_month_bills = order.generate_initial_bill() -# self.assertEqual(len(first_month_bills), 1) -# self.assertEqual(first_month_bills[0].amount, one_time_price + recurring_price) - -# # Generate & check bill for second month: full recurring_price. -# second_month_bills = Bill.generate_for(2020, 4, self.user) -# self.assertEqual(len(second_month_bills), 1) -# self.assertEqual(second_month_bills[0].amount, recurring_price) - -# # Generate & check bill for third and last month: partial recurring_price. -# third_month_bills = Bill.generate_for(2020, 5, self.user) -# self.assertEqual(len(third_month_bills), 1) -# # 31 days in May. -# self.assertEqual(float(third_month_bills[0].amount), -# round(round((7/31), AMOUNT_DECIMALS) * recurring_price, AMOUNT_DECIMALS)) - -# # Check that running Bill.generate_for() twice does not create duplicates. -# self.assertEqual(len(Bill.generate_for(2020, 3, self.user)), 0) - -# def test_basic_yearly_billing(self): -# one_time_price = 10 -# recurring_price = 150 -# description = "Test Product 1" - -# starting_date = datetime.fromisoformat('2020-03-31T08:05:23') - -# # Create order to be billed. -# order = Order.objects.create( -# owner=self.user, -# starting_date=starting_date, -# recurring_period=RecurringPeriod.PER_365D, -# recurring_price=recurring_price, -# one_time_price=one_time_price, -# description=description, -# billing_address=self.billing_address) - -# # Generate & check bill for first year: recurring_price + setup. -# first_year_bills = order.generate_initial_bill() -# self.assertEqual(len(first_year_bills), 1) -# self.assertEqual(first_year_bills[0].starting_date.date(), -# date.fromisoformat('2020-03-31')) -# self.assertEqual(first_year_bills[0].ending_date.date(), -# date.fromisoformat('2021-03-30')) -# self.assertEqual(first_year_bills[0].amount, -# recurring_price + one_time_price) - -# # Generate & check bill for second year: recurring_price. -# second_year_bills = Bill.generate_for(2021, 3, self.user) -# self.assertEqual(len(second_year_bills), 1) -# self.assertEqual(second_year_bills[0].starting_date.date(), -# date.fromisoformat('2021-03-31')) -# self.assertEqual(second_year_bills[0].ending_date.date(), -# date.fromisoformat('2022-03-30')) -# self.assertEqual(second_year_bills[0].amount, recurring_price) - -# # Check that running Bill.generate_for() twice does not create duplicates. -# self.assertEqual(len(Bill.generate_for(2020, 3, self.user)), 0) -# self.assertEqual(len(Bill.generate_for(2020, 4, self.user)), 0) -# self.assertEqual(len(Bill.generate_for(2020, 2, self.user)), 0) -# self.assertEqual(len(Bill.generate_for(2021, 3, self.user)), 0) - -# def test_basic_hourly_billing(self): -# one_time_price = 10 -# recurring_price = 1.4 -# description = "Test Product 1" - -# starting_date = datetime.fromisoformat('2020-03-31T08:05:23') -# ending_date = datetime.fromisoformat('2020-04-01T11:13:32') - -# # Create order to be billed. -# order = Order.objects.create( -# owner=self.user, -# starting_date=starting_date, -# ending_date=ending_date, -# recurring_period=RecurringPeriod.PER_HOUR, -# recurring_price=recurring_price, -# one_time_price=one_time_price, -# description=description, -# billing_address=self.billing_address) - -# # Generate & check bill for first month: recurring_price + setup. -# first_month_bills = order.generate_initial_bill() -# self.assertEqual(len(first_month_bills), 1) -# self.assertEqual(float(first_month_bills[0].amount), -# round(16 * recurring_price, AMOUNT_DECIMALS) + one_time_price) - -# # Generate & check bill for first month: recurring_price. -# second_month_bills = Bill.generate_for(2020, 4, self.user) -# self.assertEqual(len(second_month_bills), 1) -# self.assertEqual(float(second_month_bills[0].amount), -# round(12 * recurring_price, AMOUNT_DECIMALS))