generated from coulomb/repo-seed
Add file-based Bubble, Stripe, and OpenRouter importers; usage attribution, cost allocation, pricing simulator, credit wallets, and recommendations in the dashboard API. Document whynot-design UI workflow and archive the finished workplan with all ten tasks marked done.
48 lines
1.4 KiB
Python
48 lines
1.4 KiB
Python
from __future__ import annotations
|
|
|
|
from .models import MembershipRecord
|
|
|
|
|
|
def _period_prefix(date: str | None) -> str | None:
|
|
if not date or len(date) < 7:
|
|
return None
|
|
return date[:7]
|
|
|
|
|
|
def build_membership_analytics(
|
|
members: list[MembershipRecord],
|
|
period: str,
|
|
history: list[str],
|
|
) -> dict:
|
|
active = sum(1 for member in members if member.status == "active")
|
|
new_members = sum(1 for member in members if _period_prefix(member.joined_at) == period)
|
|
churned = sum(1 for member in members if _period_prefix(member.churned_at) == period)
|
|
|
|
prior_period = None
|
|
sorted_periods = sorted(history)
|
|
if period in sorted_periods:
|
|
index = sorted_periods.index(period)
|
|
if index > 0:
|
|
prior_period = sorted_periods[index - 1]
|
|
|
|
prior_active = active
|
|
if prior_period:
|
|
prior_active = sum(
|
|
1
|
|
for member in members
|
|
if member.status == "active" and _period_prefix(member.joined_at) <= prior_period
|
|
)
|
|
|
|
growth_rate = None
|
|
if prior_active:
|
|
growth_rate = round(((active - prior_active) / prior_active) * 100, 1)
|
|
|
|
return {
|
|
"period": period,
|
|
"total_members": len(members),
|
|
"active_members": active,
|
|
"new_members": new_members,
|
|
"churned_members": churned,
|
|
"growth_rate_pct": growth_rate,
|
|
"snapshot_source": "membership.json",
|
|
} |