110 lines
4.7 KiB
Python
110 lines
4.7 KiB
Python
"""Monetization models: Subscription, Transaction, Boost.
|
|
|
|
Stripe is source of truth for subscriptions. Local rows mirror state
|
|
and are reconciled via webhooks + nightly job.
|
|
Money stored as integer cents throughout.
|
|
"""
|
|
from datetime import datetime
|
|
from app.utils.time import utcnow
|
|
from sqlalchemy import JSON
|
|
from app.extensions import db
|
|
|
|
|
|
class Subscription(db.Model):
|
|
__tablename__ = "subscriptions"
|
|
|
|
id = db.Column(db.BigInteger().with_variant(db.Integer, "sqlite"),
|
|
primary_key=True, autoincrement=True)
|
|
user_id = db.Column(db.BigInteger().with_variant(db.Integer, "sqlite"),
|
|
db.ForeignKey("users.id"), nullable=False,
|
|
unique=True, index=True)
|
|
plan_id = db.Column(db.BigInteger().with_variant(db.Integer, "sqlite"),
|
|
db.ForeignKey("plans.id"), nullable=False)
|
|
stripe_customer_id = db.Column(db.String(64), nullable=True, index=True)
|
|
stripe_sub_id = db.Column(db.String(64), nullable=True, unique=True, index=True)
|
|
status = db.Column(
|
|
db.Enum("active", "past_due", "canceled", "trialing",
|
|
name="sub_status"),
|
|
nullable=False, default="active")
|
|
current_period_end = db.Column(db.DateTime, nullable=True)
|
|
cancel_at_period_end = db.Column(db.Boolean, nullable=False, default=False)
|
|
created_at = db.Column(db.DateTime, default=utcnow, nullable=False)
|
|
updated_at = db.Column(db.DateTime, default=utcnow,
|
|
onupdate=utcnow, nullable=False)
|
|
|
|
user = db.relationship("User",
|
|
backref=db.backref("subscription", uselist=False))
|
|
plan = db.relationship("Plan",
|
|
backref=db.backref("subscriptions", lazy="dynamic"))
|
|
|
|
@property
|
|
def is_active(self):
|
|
return self.status in ("active", "trialing")
|
|
|
|
def __repr__(self):
|
|
return f"<Subscription u{self.user_id} {self.status}>"
|
|
|
|
|
|
class Transaction(db.Model):
|
|
__tablename__ = "transactions"
|
|
|
|
id = db.Column(db.BigInteger().with_variant(db.Integer, "sqlite"),
|
|
primary_key=True, autoincrement=True)
|
|
user_id = db.Column(db.BigInteger().with_variant(db.Integer, "sqlite"),
|
|
db.ForeignKey("users.id"), nullable=False, index=True)
|
|
type = db.Column(
|
|
db.Enum("subscription", "boost", "refund", name="txn_type"),
|
|
nullable=False)
|
|
amount_cents = db.Column(db.Integer, nullable=False, default=0)
|
|
currency = db.Column(db.String(3), nullable=False, default="usd")
|
|
stripe_object_id = db.Column(db.String(64), nullable=True, index=True)
|
|
status = db.Column(db.String(20), nullable=False, default="succeeded")
|
|
meta = db.Column(JSON, nullable=True)
|
|
created_at = db.Column(db.DateTime, default=utcnow, nullable=False)
|
|
|
|
user = db.relationship("User",
|
|
backref=db.backref("transactions", lazy="dynamic"))
|
|
|
|
def __repr__(self):
|
|
return f"<Transaction {self.type} ${self.amount_cents/100:.2f}>"
|
|
|
|
|
|
class Boost(db.Model):
|
|
__tablename__ = "boosts"
|
|
|
|
# Boost types and their default durations in days
|
|
TYPES = {
|
|
"featured": {"label": "Featured listing", "days": 7, "cents": 499},
|
|
"bump": {"label": "Bump to top", "days": 3, "cents": 199},
|
|
"highlight":{"label": "Highlight", "days": 7, "cents": 299},
|
|
"urgent": {"label": "Urgent tag", "days": 7, "cents": 199},
|
|
}
|
|
|
|
id = db.Column(db.BigInteger().with_variant(db.Integer, "sqlite"),
|
|
primary_key=True, autoincrement=True)
|
|
listing_id = db.Column(db.BigInteger().with_variant(db.Integer, "sqlite"),
|
|
db.ForeignKey("listings.id"), nullable=False, index=True)
|
|
user_id = db.Column(db.BigInteger().with_variant(db.Integer, "sqlite"),
|
|
db.ForeignKey("users.id"), nullable=False)
|
|
type = db.Column(
|
|
db.Enum("featured", "bump", "highlight", "urgent", name="boost_type"),
|
|
nullable=False)
|
|
expires_at = db.Column(db.DateTime, nullable=False, index=True)
|
|
transaction_id = db.Column(
|
|
db.BigInteger().with_variant(db.Integer, "sqlite"),
|
|
db.ForeignKey("transactions.id"), nullable=True)
|
|
created_at = db.Column(db.DateTime, default=utcnow, nullable=False)
|
|
|
|
listing = db.relationship("Listing",
|
|
backref=db.backref("boosts", lazy="selectin"))
|
|
user = db.relationship("User",
|
|
backref=db.backref("boosts", lazy="dynamic"))
|
|
transaction = db.relationship("Transaction")
|
|
|
|
@property
|
|
def is_active(self):
|
|
return self.expires_at > utcnow()
|
|
|
|
def __repr__(self):
|
|
return f"<Boost {self.type} L{self.listing_id} exp={self.expires_at.date()}>"
|