From 50fd9e1f37867b5df03e2d823d21f6f216a2df97 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Wed, 7 Oct 2020 00:54:56 +0200 Subject: [PATCH] ++work --- .../management/commands/import-vat-rates.py | 4 +- uncloud_pay/models.py | 329 ++++++------------ 2 files changed, 103 insertions(+), 230 deletions(-) diff --git a/uncloud_pay/management/commands/import-vat-rates.py b/uncloud_pay/management/commands/import-vat-rates.py index 32938e4..2eaf80b 100644 --- a/uncloud_pay/management/commands/import-vat-rates.py +++ b/uncloud_pay/management/commands/import-vat-rates.py @@ -20,8 +20,8 @@ class Command(BaseCommand): if line_count == 0: line_count += 1 obj, created = VATRate.objects.get_or_create( - start_date=row["start_date"], - stop_date=row["stop_date"] if row["stop_date"] is not "" else None, + starting_date=row["start_date"], + ending_date=row["stop_date"] if row["stop_date"] is not "" else None, territory_codes=row["territory_codes"], currency_code=row["currency_code"], rate=row["rate"], diff --git a/uncloud_pay/models.py b/uncloud_pay/models.py index 15613a7..36057d3 100644 --- a/uncloud_pay/models.py +++ b/uncloud_pay/models.py @@ -18,7 +18,6 @@ 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 @@ -64,12 +63,9 @@ def start_after(a_date): def default_payment_delay(): return timezone.now() + BILL_PAYMENT_DELAY - class Currency(models.TextChoices): """ - We don't support months are years, because they vary in length. - This is not only complicated, but also unfair to the user, as the user pays the same - amount for different durations. + Possible currencies to be billed """ CHF = 'CHF', _('Swiss Franc') EUR = 'EUR', _('Euro') @@ -97,12 +93,32 @@ def get_balance_for_user(user): 0) return payments - bills +### +# Stripe + class StripeCustomer(models.Model): owner = models.OneToOneField( get_user_model(), primary_key=True, on_delete=models.CASCADE) stripe_id = models.CharField(max_length=32) +### +# Hosting company configuration + +class HostingProvider(models.Model): + """ + A class resembling who is running this uncloud instance. + This might change over time so we allow starting/ending dates + + This also defines the taxation rules + + WIP. + """ + starting_date = models.DateField() + ending_date = models.DateField() + + + ### # Payments and Payment Methods. @@ -393,6 +409,55 @@ class Product(UncloudModel): 'one_time_price_per_unit': 0, 'recurring_price_per_unit': 15/1000 }, + 'additional_ipv4_address': + { 'min': 0, + 'one_time_price_per_unit': 0, + 'recurring_price_per_unit': 8 + }, + } + } + ) + + obj.recurring_periods.add(recurring_period, through_defaults= { 'is_default': True }) + + obj, created = cls.objects.get_or_create(name="Dual Stack Virtual Machine v2", + description="A standard virtual machine", + currency=Currency.CHF, + config={ + 'features': { + 'base': + { 'min': 1, + 'max': 1, + 'one_time_price_per_unit': 0, + 'recurring_price_per_unit': 1 + }, + 'cores': + { 'min': 1, + 'max': 48, + 'one_time_price_per_unit': 0, + 'recurring_price_per_unit': 3 + }, + 'ram_gb': + { 'min': 1, + 'max': 256, + 'one_time_price_per_unit': 0, + 'recurring_price_per_unit': 4 + }, + 'ssd_gb': + { 'min': 10, + 'one_time_price_per_unit': 0, + 'recurring_price_per_unit': 0.35 + }, + 'hdd_gb': + { 'min': 0, + 'one_time_price_per_unit': 0, + 'recurring_price_per_unit': 15/1000 + }, + 'additional_ipv4_address': + { 'min': 0, + 'one_time_price_per_unit': 0, + 'recurring_price_per_unit': 9 + }, } } ) @@ -544,6 +609,8 @@ class Product(UncloudModel): """ + # FIXME: This logic needs to be phased out / replaced by product specific (?) + # proportions. Maybe using the RecurringPeriod table to link the possible discounts/add ups if self.default_recurring_period == RecurringPeriod.PER_365D: if requested_period == RecurringPeriod.PER_365D: @@ -669,21 +736,26 @@ class Order(models.Model): return self.starting_date + datetime.timedelta(seconds=self.recurring_period.duration_seconds) - @property - def next_cancel_or_downgrade_date(self): + + def next_cancel_or_downgrade_date(self, until_when=None): """ Return the next proper ending date after n times the recurring_period, where n is an integer that applies for downgrading or cancelling. """ - if self.recurring_period.seconds > 0: - now = timezone.now() - delta = now - self.starting_date + if not until_when: + until_when = timezone.now() - num_times = ceil(delta.total_seconds() / self.recurring_period) + if until_when < self.starting_date: + raise ValidationError("Cannot end before start of start of order") - next_date = self.starting_date + datetime.timedelta(seconds= num_times * self.recurring_period) + if self.recurring_period.duration_seconds > 0: + delta = until_when - self.starting_date + + num_times = ceil(delta.total_seconds() / self.recurring_period.duration_seconds) + + next_date = self.starting_date + datetime.timedelta(seconds=num_times * self.recurring_period.duration_seconds) else: next_date = self.starting_date @@ -694,12 +766,13 @@ class Order(models.Model): Determine the ending date given a specific bill """ - # If the order is quit, charge the final amount (?) + # If the order is quit, charge the final amount / finish (????) + # Probably not a good idea -- FIXME :continue until usual if self.ending_date: this_ending_date = self.ending_date else: - if self.earliest_ending_date > bill.ending_date: - this_ending_date = self.earliest_ending_date + if self.next_cancel_or_downgrade_date(bill.ending_date) > bill.ending_date: + this_ending_date = self.next_cancel_or_downgrade_date(bill.ending_date) else: this_ending_date = bill.ending_date @@ -796,6 +869,7 @@ class Order(models.Model): def create_bill_record(self, bill): br = None + # Note: check for != 0 not > 0, as we allow discounts to be expressed with < 0 if self.one_time_price != 0 and self.billrecord_set.count() == 0: br = BillRecord.objects.create(bill=bill, order=self, @@ -808,7 +882,6 @@ class Order(models.Model): if br: self.update_bill_record_for_recurring_order(br, bill) - else: br = self.create_new_bill_record_for_recurring_order(bill) @@ -948,7 +1021,9 @@ class Bill(models.Model): on_delete=models.CASCADE, editable=True, null=False) + # FIXME: editable=True -> is in the admin, but also editable in DRF + # Maybe filter fields in the serializer? is_final = models.BooleanField(default=False) @@ -960,9 +1035,6 @@ class Bill(models.Model): name='one_bill_per_month_per_user') ] - def __str__(self): - return f"Bill {self.owner}-{self.id}" - def close(self): """ Close/finish a bill @@ -1001,7 +1073,6 @@ class Bill(models.Model): @classmethod def create_next_bill_for_user_address(cls, billing_address, ending_date=None): - """ Create the next bill for a specific billing address of a user """ @@ -1021,6 +1092,12 @@ class Bill(models.Model): @classmethod def get_or_create_bill(cls, billing_address, ending_date=None): + """ + Get / reuse last bill if it is not yet closed + + Create bill, if there is no bill or if bill is closed. + """ + last_bill = cls.objects.filter(billing_address=billing_address).order_by('id').last() all_orders = Order.objects.filter(billing_address=billing_address).order_by('id') @@ -1056,163 +1133,8 @@ class Bill(models.Model): return bill - # @classmethod - # def create_bill_records_for_recurring_orders(cls, bill): - # """ - # Create or update bill records for recurring orders for the - # given bill - # """ - - # owner = bill.owner - # billing_address = bill.billing_address - - # all_orders = Order.objects.filter(owner=owner, - # billing_address=billing_address).order_by('id').exclude(recurring_period=RecurringPeriod.ONE_TIME) - - # for order in all_orders: - # bill_record_for_this_bill = BillRecord.objects.filter(bill=bill, - # order=order).first() - # if bill_record_for_this_bill: - # cls.update_bill_record_for_this_bill(bill_record_for_this_bill, - # order, - # bill) - - # else: - # cls.create_new_bill_record_for_this_bill(order, bill) - - - @classmethod - def create_next_bill_for_user_address_old(cls, - owner, - billing_address, - ending_date=None): - """ - Filtering ideas (TBD): - If order is replaced, it should not be added anymore if it has been billed "last time" - If order has ended and finally charged, do not charge anymore - - Find out the last billrecord for the order, if there is none, bill from the starting date - """ - - last_bill = cls.objects.filter(owner=owner, billing_address=billing_address).order_by('id').last() - - # it is important to sort orders here, as bill records will be - # created (and listed) in this order - all_orders = Order.objects.filter(owner=owner, - billing_address=billing_address).order_by('id') - first_order = all_orders.first() - - bill = None - ending_date = None - - # Get date & bill from previous bill, if it exists - if last_bill: - if not last_bill.is_final: - bill = last_bill - starting_date = last_bill.starting_date - - # FIXME: take given (parameter) or existing ending_date? - ending_date = bill.ending_date - else: - starting_date = last_bill.ending_date + datetime.timedelta(seconds=1) - else: - if first_order: - starting_date = first_order.starting_date - else: - starting_date = timezone.now() - - if not ending_date: - ending_date = end_of_month(starting_date) - - if not bill: - bill = cls.objects.create( - owner=owner, - starting_date=starting_date, - ending_date=ending_date, - billing_address=billing_address) - - for order in all_orders: - if order.is_one_time: - # this code should be ok, but needs to be double checked - if order.billrecord_set.count() == 0: - br = BillRecord.objects.create(bill=bill, - order=order, - starting_date=order.starting_date, - ending_date=order.ending_date) - - else: # recurring orders - bill_record_for_this_bill = BillRecord.objects.filter(bill=bill, - order=order).first() - - # This bill already has a bill record for the order - # We potentially need to update the ending_date if the ending_date - # of the bill changed. - if bill_record_for_this_bill: - # we may need to adjust it, but let's do this logic another time - - # If the order has an ending date set, we might need to adjust the bill_record - if order.ending_date: - if bill_record_for_this_bill.ending_date != order.ending_date: - bill_record_for_this_bill.ending_date = order.ending_date - - else: - # recurring, not terminated, should go until at least end of bill - if bill_record_for_this_bill.ending_date < bill.ending_date: - bill_record_for_this_bill.ending_date = bill.ending_date - - - bill_record_for_this_bill.save() - - else: # No bill record in this bill for this order yet - # Find out when billed last time - last_bill_record = BillRecord.objects.filter(order=order).order_by('id').last() - - # Default starting date - this_starting_date=order.starting_date - - # Skip billing again, if we have been processed for this bill duration - if last_bill_record: - if last_bill_record.ending_date >= bill.ending_date: - continue - - # If the order ended and has been fully billed - do not process it - # anymore - if order.ending_date: - if last_bill_record.ending_date == order.ending_date: - # FIXME: maybe mark order for not processing anymore? - # I imagina a boolean column, once this code is stable and - # verified - continue - - # Catch programming bugs if the last bill_record was - # created incorrectly - should never be entered! - if order.ending_date < last_bill_record.ending_date: - raise ValidationError(f"Order {order.id} ends before last bill record {last_bill_record.id}") - - # Start right after last billing run - this_starting_date = last_bill_record.ending_date + datetime.timedelta(seconds=1) - - - # If the order is already terminated, use that date instead of bill date - if order.ending_date: - this_ending_date = order.ending_date - else: - if order.earliest_ending_date > bill.ending_date: - this_ending_date = order.earliest_ending_date - else: - # bill at maximum for this billing period - this_ending_date = bill.ending_date - - # And finally create a new billrecord! - br = BillRecord.objects.create(bill=bill, - order=order, - starting_date=this_starting_date, - ending_date=this_ending_date) - - - - - return bill + def __str__(self): + return f"Bill {self.owner}-{self.id}" class BillRecord(models.Model): @@ -1261,10 +1183,10 @@ class BillRecord(models.Model): super().save(*args, **kwargs) - class ProductToRecurringPeriod(models.Model): """ - Intermediate manytomany mapping class + Intermediate manytomany mapping class that allows storing the default recurring period + for a product """ recurring_period = models.ForeignKey(RecurringPeriod, on_delete=models.CASCADE) @@ -1283,52 +1205,3 @@ class ProductToRecurringPeriod(models.Model): def __str__(self): return f"{self.product} - {self.recurring_period} (default: {self.is_default})" - - -# # Sample products included into uncloud -# class SampleOneTimeProduct(models.Model): -# """ -# Products are usually more complex, but this product shows how easy -# it can be to create your own one time product. -# """ - -# default_recurring_period = RecurringPeriod.ONE_TIME - -# ot_price = models.IntegerField(default=5) - -# @property -# def one_time_price(self): -# return self.ot_price - -# class SampleRecurringProduct(models.Model): -# """ -# Products are usually more complex, but this product shows how easy -# it can be to create your own recurring fee product. -# """ - -# default_recurring_period = RecurringPeriod.PER_30D - -# rc_price = models.IntegerField(default=10) - -# @property -# def recurring_price(self): -# return self.rc_price - -# class SampleRecurringProductOneTimeFee(models.Model): -# """ -# Products are usually more complex, but this product shows how easy -# it can be to create your own one time + recurring fee product. -# """ - -# default_recurring_period = RecurringPeriod.PER_30D - -# ot_price = models.IntegerField(default=5) -# rc_price = models.IntegerField(default=10) - -# @property -# def one_time_price(self): -# return self.ot_price - -# @property -# def recurring_price(self): -# return self.rc_price