diff --git a/corporate/models.py b/corporate/models.py index d90d8ce90b..5ed704d0a6 100644 --- a/corporate/models.py +++ b/corporate/models.py @@ -22,8 +22,10 @@ class Customer(models.Model): default_discount: Optional[Decimal] = models.DecimalField( decimal_places=4, max_digits=7, null=True ) - # Some non-profit organizations on manual license management pay only for their paid employees. - # We don't prevent these organizations from adding more users than the number of licenses they purchased. + # Some non-profit organizations on manual license management pay + # only for their paid employees. We don't prevent these + # organizations from adding more users than the number of licenses + # they purchased. exempt_from_from_license_number_check: bool = models.BooleanField(default=False) def __str__(self) -> str: @@ -57,18 +59,39 @@ class CustomerPlan(models.Model): # Discount that was applied. For display purposes only. discount: Optional[Decimal] = models.DecimalField(decimal_places=4, max_digits=6, null=True) + # Initialized with the time of plan creation. Used for calculating + # start of next billing cycle, next invoice date etc. This value + # should never be modified. The only exception is when we change + # the status of the plan from free trial to active and reset the + # billing_cycle_anchor. billing_cycle_anchor: datetime.datetime = models.DateTimeField() + ANNUAL = 1 MONTHLY = 2 billing_schedule: int = models.SmallIntegerField() + # The next date the billing system should go through ledger + # entries and create invoices for additional users or plan + # renewal. Since we use a daily cron job for invoicing, the + # invoice will be generated the first time the cron job runs after + # next_invoice_date. next_invoice_date: Optional[datetime.datetime] = models.DateTimeField(db_index=True, null=True) + + # On next_invoice_date, we go through ledger entries that were + # created after invoiced_through and process them by generating + # invoices for any additional users and/or plan renewal. Once the + # invoice is generated, we update the value of invoiced_through + # and set it to the last ledger entry we processed. invoiced_through: Optional["LicenseLedger"] = models.ForeignKey( "LicenseLedger", null=True, on_delete=CASCADE, related_name="+" ) + DONE = 1 STARTED = 2 INITIAL_INVOICE_TO_BE_SENT = 3 + # This status field helps ensure any errors encountered during the + # invoicing process do not leave our invoicing system in a broken + # state. invoicing_status: int = models.SmallIntegerField(default=DONE) STANDARD = 1 @@ -87,7 +110,8 @@ class CustomerPlan(models.Model): NEVER_STARTED = 12 status: int = models.SmallIntegerField(default=ACTIVE) - # TODO maybe override setattr to ensure billing_cycle_anchor, etc are immutable + # TODO maybe override setattr to ensure billing_cycle_anchor, etc + # are immutable. @property def name(self) -> str: @@ -147,10 +171,21 @@ class LicenseLedger(models.Model): """ plan: CustomerPlan = models.ForeignKey(CustomerPlan, on_delete=CASCADE) + # Also True for the initial upgrade. is_renewal: bool = models.BooleanField(default=False) + event_time: datetime.datetime = models.DateTimeField() + + # The number of licenses ("seats") purchased by the the organization at the time of ledger + # entry creation. Normally, to add a user the organization needs at least one spare license. + # Once a license is purchased, it is valid till the end of the billing period, irrespective + # of whether the license is used or not. So the value of licenses will never decrease for + # subsequent LicenseLedger entries in the same billing period. licenses: int = models.IntegerField() - # None means the plan does not automatically renew. - # This cannot be None if plan.automanage_licenses. + + # The number of licenses the organization needs in the next billing cycle. The value of + # licenses_at_next_renewal can increase or decrease for subsequent LicenseLedger entries in + # the same billing period. For plans on automatic license management this value is usually + # equal to the number of activated users in the organization. licenses_at_next_renewal: Optional[int] = models.IntegerField(null=True)