diff --git a/company_site/financial/models.py b/company_site/financial/models.py index 8f3478d..40ac236 100755 --- a/company_site/financial/models.py +++ b/company_site/financial/models.py @@ -302,6 +302,108 @@ class TimeCardCell(IdMixin, TimeMixin): charge_number = models.ForeignKey(ChargeNumber, on_delete=models.CASCADE, null=True, blank=True) +class BillingCustomer(IdMixin, TimeMixin): + """Customer who pays us (one-off invoices or subscriptions).""" + + name = models.CharField(max_length=200) + email = models.EmailField() + company = models.CharField(max_length=200, blank=True) + stripe_customer_id = models.CharField(max_length=255, blank=True, default="") + notes = models.TextField(blank=True) + + class Meta: + ordering = ["name"] + + def __str__(self): + return f"{self.name} <{self.email}>" + + +class Invoice(IdMixin, TimeMixin): + """One-off invoice — customer pays us via Stripe hosted invoice URL.""" + + class Status(models.TextChoices): + DRAFT = "draft", "Draft" + OPEN = "open", "Open" + PAID = "paid", "Paid" + VOID = "void", "Void" + UNCOLLECTIBLE = "uncollectible", "Uncollectible" + FAILED = "failed", "Failed" + + customer = models.ForeignKey( + BillingCustomer, on_delete=models.PROTECT, related_name="invoices" + ) + description = models.CharField(max_length=500) + amount_cents = models.PositiveIntegerField(help_text="Amount in cents (USD)") + currency = models.CharField(max_length=3, default="usd") + status = models.CharField( + max_length=20, choices=Status.choices, default=Status.DRAFT + ) + due_date = models.DateField(null=True, blank=True) + notes = models.TextField(blank=True) + stripe_invoice_id = models.CharField(max_length=255, blank=True, default="") + hosted_invoice_url = models.URLField(max_length=500, blank=True, default="") + invoice_pdf_url = models.URLField(max_length=500, blank=True, default="") + pay_link_emailed_at = models.DateTimeField(null=True, blank=True) + + class Meta: + ordering = ["-created"] + + def __str__(self): + return f"Invoice {self.pk} — {self.customer} — {self.status}" + + @property + def amount_dollars(self): + return self.amount_cents / 100.0 + + +class RecurringSubscription(IdMixin, TimeMixin): + """Monthly (or custom) recurring charge — customer pays us via Stripe Subscription.""" + + class Status(models.TextChoices): + INCOMPLETE = "incomplete", "Incomplete" + ACTIVE = "active", "Active" + PAST_DUE = "past_due", "Past due" + CANCELED = "canceled", "Canceled" + UNPAID = "unpaid", "Unpaid" + TRIALING = "trialing", "Trialing" + PAUSED = "paused", "Paused" + + class Interval(models.TextChoices): + MONTH = "month", "Monthly" + YEAR = "year", "Yearly" + WEEK = "week", "Weekly" + + customer = models.ForeignKey( + BillingCustomer, on_delete=models.PROTECT, related_name="subscriptions" + ) + description = models.CharField(max_length=500) + amount_cents = models.PositiveIntegerField(help_text="Amount per period in cents (USD)") + currency = models.CharField(max_length=3, default="usd") + interval = models.CharField( + max_length=10, choices=Interval.choices, default=Interval.MONTH + ) + status = models.CharField( + max_length=20, choices=Status.choices, default=Status.INCOMPLETE + ) + notes = models.TextField(blank=True) + stripe_product_id = models.CharField(max_length=255, blank=True, default="") + stripe_price_id = models.CharField(max_length=255, blank=True, default="") + stripe_subscription_id = models.CharField(max_length=255, blank=True, default="") + checkout_session_id = models.CharField(max_length=255, blank=True, default="") + checkout_url = models.URLField(max_length=500, blank=True, default="") + pay_link_emailed_at = models.DateTimeField(null=True, blank=True) + + class Meta: + ordering = ["-created"] + + def __str__(self): + return f"Subscription {self.pk} — {self.customer} — {self.status}" + + @property + def amount_dollars(self): + return self.amount_cents / 100.0 + + def set_user_type(user, user_type): """Set user type and sync the Employee record (mutually exclusive types).""" user.__dict__.pop("profile", None)