innercontext/backend/innercontext/services/pricing_jobs.py

93 lines
2.7 KiB
Python

from datetime import datetime
from sqlmodel import Session, col, select
from innercontext.models import PricingRecalcJob, Product
from innercontext.models.base import utc_now
def enqueue_pricing_recalc(
session: Session, *, scope: str = "global"
) -> PricingRecalcJob:
existing = session.exec(
select(PricingRecalcJob)
.where(PricingRecalcJob.scope == scope)
.where(col(PricingRecalcJob.status).in_(["pending", "running"]))
.order_by(col(PricingRecalcJob.created_at).asc())
).first()
if existing is not None:
return existing
job = PricingRecalcJob(scope=scope, status="pending")
session.add(job)
return job
def claim_next_pending_pricing_job(session: Session) -> PricingRecalcJob | None:
stmt = (
select(PricingRecalcJob)
.where(PricingRecalcJob.status == "pending")
.order_by(col(PricingRecalcJob.created_at).asc())
)
bind = session.get_bind()
if bind is not None and bind.dialect.name == "postgresql":
stmt = stmt.with_for_update(skip_locked=True)
job = session.exec(stmt).first()
if job is None:
return None
job.status = "running"
job.attempts += 1
job.started_at = utc_now()
job.finished_at = None
job.error = None
session.add(job)
session.commit()
session.refresh(job)
return job
def _apply_pricing_snapshot(session: Session, computed_at: datetime) -> int:
from innercontext.api.products import _compute_pricing_outputs
products = list(session.exec(select(Product)).all())
pricing_outputs = _compute_pricing_outputs(products)
for product in products:
tier, price_per_use_pln, tier_source = pricing_outputs.get(
product.id, (None, None, None)
)
product.price_tier = tier
product.price_per_use_pln = price_per_use_pln
product.price_tier_source = tier_source
product.pricing_computed_at = computed_at
return len(products)
def process_pricing_job(session: Session, job: PricingRecalcJob) -> int:
try:
updated_count = _apply_pricing_snapshot(session, computed_at=utc_now())
job.status = "succeeded"
job.finished_at = utc_now()
job.error = None
session.add(job)
session.commit()
return updated_count
except Exception as exc:
session.rollback()
job.status = "failed"
job.finished_at = utc_now()
job.error = str(exc)[:512]
session.add(job)
session.commit()
raise
def process_one_pending_pricing_job(session: Session) -> bool:
job = claim_next_pending_pricing_job(session)
if job is None:
return False
process_pricing_job(session, job)
return True