06/15 Phase 4 + 5 codes

This commit is contained in:
2026-06-15 17:43:47 -04:00
parent f57a95013c
commit a9efd15a76
36 changed files with 1677 additions and 57 deletions
+4 -1
View File
@@ -7,7 +7,10 @@ from app.models.listing import Listing, ListingImage
from app.models.geo import ZipGeo, Metro
from app.models.messaging import Conversation, Message
from app.models.favorite import Favorite
from app.models.payments import Subscription, Transaction, Boost
from app.models.ads import Ad, Sponsor, PromotedKeyword
__all__ = ["Plan", "User", "TrustEvent", "Category", "Listing",
"ListingImage", "ZipGeo", "Metro", "Conversation", "Message",
"Favorite"]
"Favorite", "Subscription", "Transaction", "Boost",
"Ad", "Sponsor", "PromotedKeyword"]
+117
View File
@@ -0,0 +1,117 @@
"""Ad and Sponsor models.
Ad: internal ad server. Slots: header, sidebar, inline, footer.
Targeting: lang (nullable = all), geo_state (nullable = all).
Impression/click counters on the model; Redis batching added in Phase 7.
Sponsor: paid directory listing + optional category sponsorship.
Category sponsor FK wired to categories.sponsor_id (set separately).
PromotedKeyword: pinned listing for a search keyword (promoted search).
"""
from datetime import datetime
from app.extensions import db
class Ad(db.Model):
__tablename__ = "ads"
SLOTS = ["header", "sidebar", "inline", "footer"]
id = db.Column(db.BigInteger().with_variant(db.Integer, "sqlite"),
primary_key=True, autoincrement=True)
advertiser_name = db.Column(db.String(120), nullable=False)
slot = db.Column(db.Enum("header", "sidebar", "inline", "footer",
name="ad_slot"), nullable=False)
creative_path = db.Column(db.String(255), nullable=True) # image file rel path
target_url = db.Column(db.String(512), nullable=False)
alt_text = db.Column(db.String(200), nullable=True)
# targeting (null = match all)
lang = db.Column(db.String(5), nullable=True)
geo_state = db.Column(db.String(2), nullable=True)
starts_at = db.Column(db.DateTime, nullable=False, index=True)
ends_at = db.Column(db.DateTime, nullable=False, index=True)
impressions = db.Column(db.Integer, nullable=False, default=0)
clicks = db.Column(db.Integer, nullable=False, default=0)
is_active = db.Column(db.Boolean, nullable=False, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated_at = db.Column(db.DateTime, default=datetime.utcnow,
onupdate=datetime.utcnow, nullable=False)
@property
def ctr(self):
if not self.impressions:
return 0.0
return round(self.clicks / self.impressions * 100, 2)
@property
def is_running(self):
now = datetime.utcnow()
return self.is_active and self.starts_at <= now <= self.ends_at
def __repr__(self):
return f"<Ad {self.id} slot={self.slot} adv={self.advertiser_name[:20]}>"
class Sponsor(db.Model):
__tablename__ = "sponsors"
TIERS = ["directory", "category"]
id = db.Column(db.BigInteger().with_variant(db.Integer, "sqlite"),
primary_key=True, autoincrement=True)
name = db.Column(db.String(120), nullable=False)
logo_path = db.Column(db.String(255), nullable=True)
url = db.Column(db.String(512), nullable=False)
tagline = db.Column(db.String(160), nullable=True)
tier = db.Column(db.Enum("directory", "category", name="sponsor_tier"),
nullable=False, default="directory")
category_id = db.Column(
db.BigInteger().with_variant(db.Integer, "sqlite"),
db.ForeignKey("categories.id"), nullable=True)
starts_at = db.Column(db.DateTime, nullable=False)
ends_at = db.Column(db.DateTime, nullable=False)
is_active = db.Column(db.Boolean, nullable=False, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
category = db.relationship("Category",
backref=db.backref("sponsors", lazy="selectin"))
@property
def is_running(self):
now = datetime.utcnow()
return self.is_active and self.starts_at <= now <= self.ends_at
def __repr__(self):
return f"<Sponsor {self.name} tier={self.tier}>"
class PromotedKeyword(db.Model):
"""A listing pinned to the top of search results for a given keyword."""
__tablename__ = "promoted_keywords"
__table_args__ = (
db.UniqueConstraint("keyword", "listing_id", name="uq_pk_kw_listing"),
)
id = db.Column(db.BigInteger().with_variant(db.Integer, "sqlite"),
primary_key=True, autoincrement=True)
keyword = db.Column(db.String(80), nullable=False, index=True)
listing_id = db.Column(db.BigInteger().with_variant(db.Integer, "sqlite"),
db.ForeignKey("listings.id"), nullable=False)
priority = db.Column(db.Integer, nullable=False, default=0)
expires_at = db.Column(db.DateTime, nullable=False, index=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
listing = db.relationship("Listing",
backref=db.backref("promoted_keywords",
lazy="selectin"))
@property
def is_active(self):
return self.expires_at > datetime.utcnow()
def __repr__(self):
return f"<PromotedKeyword '{self.keyword}' L{self.listing_id}>"
+108
View File
@@ -0,0 +1,108 @@
"""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 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=datetime.utcnow, nullable=False)
updated_at = db.Column(db.DateTime, default=datetime.utcnow,
onupdate=datetime.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=datetime.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=datetime.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 > datetime.utcnow()
def __repr__(self):
return f"<Boost {self.type} L{self.listing_id} exp={self.expires_at.date()}>"