Spaces:
Running
Running
Upload 2 files
Browse files
app.py
CHANGED
|
@@ -128,17 +128,18 @@ def upsell_html(up):
|
|
| 128 |
icon = {"upgrade": "💎", "signin": "🔐", "limit": "⏳", "extra": "📄"}.get(kind, "✨")
|
| 129 |
title = html.escape(getattr(up, "headline", "") or "Heads up")
|
| 130 |
text = html.escape(getattr(up, "text", "") or str(up))
|
| 131 |
-
st = P.PLANS["
|
| 132 |
perks = {
|
| 133 |
-
"upgrade": [f"{
|
| 134 |
-
"
|
|
|
|
| 135 |
"signin": [f"{P.FREE_TRIAL_DOCS} documents free", "Word & PDF upload", "Run history & re-downloads",
|
| 136 |
"No card needed"],
|
| 137 |
-
"extra": [f"{P.price_of('
|
| 138 |
-
"Continue straight away
|
| 139 |
-
"
|
| 140 |
}.get(kind, [])
|
| 141 |
-
go = {"upgrade": ("💎 See plans", "cs-go-pricing"), "extra": ("📄 Buy
|
| 142 |
"signin": ("🔐 Sign in / Create free account", "cs-go-account")}.get(kind)
|
| 143 |
perks_html = "".join(f'<li><span>✓</span>{html.escape(x)}</li>' for x in perks)
|
| 144 |
btns = (f'<button class="cs-up-go" data-cs-go="{go[1]}">{go[0]}</button>' if go else "") + \
|
|
@@ -1698,17 +1699,15 @@ with gr.Blocks(title=f"{APP_NAME} · CS Software Solutions", theme=theme, css=CS
|
|
| 1698 |
pr_html = gr.HTML(P.pricing_html(P.DEFAULT_CURRENCY))
|
| 1699 |
_b = P.price_buttons(P.DEFAULT_CURRENCY)
|
| 1700 |
with gr.Row():
|
| 1701 |
-
|
| 1702 |
-
|
| 1703 |
-
pr_pro = gr.Button(_b[2])
|
| 1704 |
-
pr_extra = gr.Button(_b[3], size="sm")
|
| 1705 |
pr_pay = gr.HTML()
|
| 1706 |
-
gr.HTML('<div class="cs-gauge-cap" style="text-align:left;margin-top:10px">
|
| 1707 |
-
'
|
| 1708 |
-
'
|
| 1709 |
-
'
|
| 1710 |
-
'
|
| 1711 |
-
'
|
| 1712 |
|
| 1713 |
# ============================ ABOUT / CONTACT ============================
|
| 1714 |
with gr.Tab("ℹ️ About us", id="about"):
|
|
@@ -1841,14 +1840,14 @@ with gr.Blocks(title=f"{APP_NAME} · CS Software Solutions", theme=theme, css=CS
|
|
| 1841 |
'(refund, misuse, user request); the user goes back to the free plan.</div>')
|
| 1842 |
with gr.Row():
|
| 1843 |
ad_p_email = gr.Textbox(label="User email", scale=3)
|
| 1844 |
-
ad_p_plan = gr.Dropdown(["
|
| 1845 |
label="Plan", scale=2)
|
| 1846 |
ad_p_days = gr.Number(value=1, label="Months", precision=0, minimum=1, scale=1)
|
| 1847 |
ad_p_amount = gr.Textbox(label="Amount received (optional)", scale=2)
|
| 1848 |
with gr.Row():
|
| 1849 |
ad_p_activate = gr.Button("✅ Activate plan", variant="primary")
|
| 1850 |
-
ad_p_credits_n = gr.Number(value=1, label="
|
| 1851 |
-
ad_p_credits = gr.Button("➕ Add
|
| 1852 |
with gr.Row():
|
| 1853 |
ad_t_reason = gr.Textbox(label="Reason for terminating (optional, sent to the user)",
|
| 1854 |
scale=3)
|
|
@@ -2084,11 +2083,9 @@ with gr.Blocks(title=f"{APP_NAME} · CS Software Solutions", theme=theme, css=CS
|
|
| 2084 |
def _switch_currency(c):
|
| 2085 |
b = P.price_buttons(c)
|
| 2086 |
return (P.pricing_html(c),) + tuple(gr.update(value=x) for x in b)
|
| 2087 |
-
pr_cur.change(_switch_currency, pr_cur, [pr_html,
|
| 2088 |
-
**HID)
|
| 2089 |
|
| 2090 |
-
for b, item in ((
|
| 2091 |
-
(pr_extra, "Extra documents")):
|
| 2092 |
b.click(lambda u, item=item: P.pay_instructions(u, item), user_state, pr_pay, **HID)
|
| 2093 |
|
| 2094 |
# admin: plans, credits, requests, payments
|
|
|
|
| 128 |
icon = {"upgrade": "💎", "signin": "🔐", "limit": "⏳", "extra": "📄"}.get(kind, "✨")
|
| 129 |
title = html.escape(getattr(up, "headline", "") or "Heads up")
|
| 130 |
text = html.escape(getattr(up, "text", "") or str(up))
|
| 131 |
+
st = P.PLANS["monthly"]
|
| 132 |
perks = {
|
| 133 |
+
"upgrade": [f"Single document: {P.price_of('single', 'INR')} / {P.price_of('single', 'USD')}",
|
| 134 |
+
f"Monthly plan: {st['docs_month']} documents for ₹{st['inr']:,} / ${st['usd']:,.0f}",
|
| 135 |
+
"Word layout kept + PDF report", "Rigorous proofread included"],
|
| 136 |
"signin": [f"{P.FREE_TRIAL_DOCS} documents free", "Word & PDF upload", "Run history & re-downloads",
|
| 137 |
"No card needed"],
|
| 138 |
+
"extra": [f"{P.price_of('single', 'INR')} / {P.price_of('single', 'USD')} per single document",
|
| 139 |
+
"Continue straight away", "Single documents never expire",
|
| 140 |
+
"Your monthly documents start again next month"],
|
| 141 |
}.get(kind, [])
|
| 142 |
+
go = {"upgrade": ("💎 See plans", "cs-go-pricing"), "extra": ("📄 Buy single documents", "cs-go-pricing"),
|
| 143 |
"signin": ("🔐 Sign in / Create free account", "cs-go-account")}.get(kind)
|
| 144 |
perks_html = "".join(f'<li><span>✓</span>{html.escape(x)}</li>' for x in perks)
|
| 145 |
btns = (f'<button class="cs-up-go" data-cs-go="{go[1]}">{go[0]}</button>' if go else "") + \
|
|
|
|
| 1699 |
pr_html = gr.HTML(P.pricing_html(P.DEFAULT_CURRENCY))
|
| 1700 |
_b = P.price_buttons(P.DEFAULT_CURRENCY)
|
| 1701 |
with gr.Row():
|
| 1702 |
+
pr_single = gr.Button(_b[0])
|
| 1703 |
+
pr_monthly = gr.Button(_b[1], variant="primary")
|
|
|
|
|
|
|
| 1704 |
pr_pay = gr.HTML()
|
| 1705 |
+
gr.HTML('<div class="cs-gauge-cap" style="text-align:left;margin-top:10px">Prices in Indian rupees (₹) '
|
| 1706 |
+
'for India and US dollars ($) for other countries, switch with Currency above. A single document '
|
| 1707 |
+
'is a credit that never expires. The monthly plan runs for one month from the day it starts and '
|
| 1708 |
+
'includes ' + str(P.PLANS["monthly"]["docs_month"]) + ' documents for that month; unused documents '
|
| 1709 |
+
'do not carry over. A plan that is not renewed by its end date is switched off automatically. '
|
| 1710 |
+
'Results are indicators from our own detector and can differ from other checkers.</div>')
|
| 1711 |
|
| 1712 |
# ============================ ABOUT / CONTACT ============================
|
| 1713 |
with gr.Tab("ℹ️ About us", id="about"):
|
|
|
|
| 1840 |
'(refund, misuse, user request); the user goes back to the free plan.</div>')
|
| 1841 |
with gr.Row():
|
| 1842 |
ad_p_email = gr.Textbox(label="User email", scale=3)
|
| 1843 |
+
ad_p_plan = gr.Dropdown(["monthly"], value="monthly",
|
| 1844 |
label="Plan", scale=2)
|
| 1845 |
ad_p_days = gr.Number(value=1, label="Months", precision=0, minimum=1, scale=1)
|
| 1846 |
ad_p_amount = gr.Textbox(label="Amount received (optional)", scale=2)
|
| 1847 |
with gr.Row():
|
| 1848 |
ad_p_activate = gr.Button("✅ Activate plan", variant="primary")
|
| 1849 |
+
ad_p_credits_n = gr.Number(value=1, label="Single documents", precision=0)
|
| 1850 |
+
ad_p_credits = gr.Button("➕ Add single documents")
|
| 1851 |
with gr.Row():
|
| 1852 |
ad_t_reason = gr.Textbox(label="Reason for terminating (optional, sent to the user)",
|
| 1853 |
scale=3)
|
|
|
|
| 2083 |
def _switch_currency(c):
|
| 2084 |
b = P.price_buttons(c)
|
| 2085 |
return (P.pricing_html(c),) + tuple(gr.update(value=x) for x in b)
|
| 2086 |
+
pr_cur.change(_switch_currency, pr_cur, [pr_html, pr_single, pr_monthly], queue=False, **HID)
|
|
|
|
| 2087 |
|
| 2088 |
+
for b, item in ((pr_single, "Single document"), (pr_monthly, "Monthly plan")):
|
|
|
|
| 2089 |
b.click(lambda u, item=item: P.pay_instructions(u, item), user_state, pr_pay, **HID)
|
| 2090 |
|
| 2091 |
# admin: plans, credits, requests, payments
|
plans.py
CHANGED
|
@@ -4,10 +4,8 @@ Pricing plans, quotas, credits and upgrade nudges.
|
|
| 4 |
Who pays what (all paid plans are MONTHLY; ₹ in India, $ for international users)
|
| 5 |
guest : paste only, 300 words, 15 runs/day, max 2 Smart passes
|
| 6 |
free : + FREE_TRIAL_DOCS document runs (lifetime), 25 pastes/day
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
pro : ₹10,000 / $119 a month · unlimited documents a day
|
| 10 |
-
extra docs : credits at ₹99 / $2 each, used after the daily limit (Student / Agency); never expire
|
| 11 |
admin : everything free and unlimited, no nudges, never charged
|
| 12 |
|
| 13 |
Subscriptions (users table)
|
|
@@ -41,25 +39,23 @@ UNLIMITED = 10 ** 9
|
|
| 41 |
|
| 42 |
# docs_day: documents (reports) per day; 0 = unlimited
|
| 43 |
PLANS = {
|
| 44 |
-
"guest": dict(name="Guest", inr=0, usd=0, docs_day=0, pastes=15, paste_words=300, passes=2, doc_words=0),
|
| 45 |
-
"free": dict(name="Free", inr=0, usd=0, docs_day=0, pastes=25, paste_words=300, passes=2, doc_words=15000),
|
| 46 |
-
"student"
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
paste_words=3000, passes=4, doc_words=100000),
|
| 52 |
-
"pro": dict(name="Pro", inr=int(os.getenv("PRICE_PRO", "10000")),
|
| 53 |
-
usd=float(os.getenv("PRICE_PRO_USD", "119")), docs_day=0, pastes=UNLIMITED,
|
| 54 |
-
paste_words=5000, passes=4, doc_words=150000),
|
| 55 |
-
"admin": dict(name="Admin", inr=0, usd=0, docs_day=0, pastes=UNLIMITED, paste_words=20000, passes=4,
|
| 56 |
doc_words=UNLIMITED),
|
| 57 |
}
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
| 63 |
INR_NOTE = os.getenv("PRICE_INR_NOTE", "") # e.g. "Prices include 18% GST" / "+18% GST"
|
| 64 |
# Pricing page currency: INR for India (₹), USD for international visitors ($). Visitors switch on the Pricing tab.
|
| 65 |
DEFAULT_CURRENCY = os.getenv("PRICE_CURRENCY", "INR").strip().upper()
|
|
@@ -78,7 +74,7 @@ def money(amount, cur="INR", per=""):
|
|
| 78 |
|
| 79 |
|
| 80 |
def price_of(item, cur="INR"):
|
| 81 |
-
p =
|
| 82 |
return money(p["usd"] if cur == "USD" else p["inr"], cur)
|
| 83 |
|
| 84 |
|
|
@@ -90,7 +86,7 @@ def both(item):
|
|
| 90 |
|
| 91 |
|
| 92 |
def per_day_text(p):
|
| 93 |
-
return
|
| 94 |
# ---------------------------------------------------------------- storage
|
| 95 |
def _db():
|
| 96 |
con = A._db()
|
|
@@ -100,7 +96,11 @@ def _db():
|
|
| 100 |
("trial_used", "INTEGER DEFAULT 0"), ("plan_started", "TEXT"),
|
| 101 |
("plan_status", "TEXT DEFAULT 'free'")):
|
| 102 |
if c not in cols:
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
con.execute("CREATE TABLE IF NOT EXISTS payments (id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
| 105 |
"ts TEXT DEFAULT CURRENT_TIMESTAMP, email TEXT, item TEXT, amount TEXT, source TEXT, note TEXT)")
|
| 106 |
con.execute("CREATE TABLE IF NOT EXISTS upgrade_requests (id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
|
@@ -145,7 +145,7 @@ def expire_due(email=None):
|
|
| 145 |
"""Switch off every paid plan whose month ended without a renewal. Returns the emails switched off."""
|
| 146 |
now = _ts(_now())
|
| 147 |
with _db() as con:
|
| 148 |
-
q = ("SELECT email, plan, plan_started, plan_until FROM users WHERE plan IN
|
| 149 |
"AND plan_until IS NOT NULL AND plan_until <= ?")
|
| 150 |
rows = con.execute(q + (" AND email=?" if email else ""), (now, email) if email else (now,)).fetchall()
|
| 151 |
for e, plan, started, until in rows:
|
|
@@ -177,37 +177,52 @@ threading.Thread(target=_expiry_loop, daemon=True).start()
|
|
| 177 |
|
| 178 |
|
| 179 |
# ---------------------------------------------------------------- state
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
def state(user):
|
| 181 |
"""Everything the UI and the checks need about a user's plan."""
|
| 182 |
if A.is_admin(user):
|
| 183 |
-
return dict(key="admin", **PLANS["admin"], until=None, started=None, used_today=0, credits=0,
|
| 184 |
-
trial_left=0, paid=True, admin=True, status="admin")
|
| 185 |
if not user:
|
| 186 |
-
return dict(key="guest", **PLANS["guest"], until=None, started=None, used_today=0, credits=0,
|
| 187 |
-
trial_left=0, paid=False, admin=False, status="guest")
|
| 188 |
with _db() as con:
|
| 189 |
-
row = con.execute("SELECT plan, plan_until, plan_started, doc_credits, trial_used, plan_status "
|
| 190 |
-
"FROM users WHERE email=?", (user["email"],)).fetchone()
|
| 191 |
-
plan, until, started, credits, trial, status = row or ("free", None, None, 0, 0, "free")
|
| 192 |
if plan in PAID and until and _parse(until) <= _now():
|
| 193 |
expire_due(user["email"]) # month over, not renewed: switch it off now
|
| 194 |
plan, status = "free", "expired"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
p = PLANS.get(plan, PLANS["free"])
|
| 196 |
-
try:
|
| 197 |
-
used_today = A.usage_today(user["email"], "doc")
|
| 198 |
-
except Exception:
|
| 199 |
-
used_today = 0
|
| 200 |
return dict(key=plan if plan in PLANS else "free", **p, until=until if plan in PAID else None,
|
| 201 |
-
started=started if plan in PAID else None,
|
| 202 |
-
trial_left=max(0, FREE_TRIAL_DOCS - (trial or 0)),
|
| 203 |
paid=plan in PAID or (credits or 0) > 0, admin=False,
|
| 204 |
status="active" if plan in PAID else (status or "free"))
|
| 205 |
|
| 206 |
|
| 207 |
def docs_available(st):
|
| 208 |
-
"""Documents the user can still run
|
| 209 |
if st["key"] in PAID:
|
| 210 |
-
return
|
| 211 |
return st["credits"] + st["trial_left"]
|
| 212 |
|
| 213 |
|
|
@@ -221,24 +236,23 @@ def check_doc(user, words=None):
|
|
| 221 |
if st["admin"]:
|
| 222 |
return
|
| 223 |
if st["key"] in PAID:
|
| 224 |
-
if st["
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
f"
|
| 228 |
-
f"midnight (IST) when your daily documents reset. Need more every day? Move up to "
|
| 229 |
-
f"{better}.", kind="extra", title="Daily limit reached")
|
| 230 |
cap = st["doc_words"]
|
| 231 |
else:
|
| 232 |
if docs_available(st) <= 0:
|
| 233 |
-
msg = ("Your plan was not renewed, so it has been switched off. Renew it on the 💎 Pricing tab
|
| 234 |
-
"
|
| 235 |
-
"Your subscription has been ended. Please contact us, or choose
|
| 236 |
if st["status"] == "terminated" else
|
| 237 |
-
f"You've used your {FREE_TRIAL_DOCS} free documents.
|
| 238 |
-
f"
|
|
|
|
| 239 |
raise A.Upsell(msg, title="Your plan has ended" if st["status"] in ("expired", "terminated") else
|
| 240 |
"Loved your free documents?")
|
| 241 |
-
cap = PLANS["free"]["doc_words"]
|
| 242 |
if words and words > cap:
|
| 243 |
raise A.Upsell(f"This document has {words:,} words; your plan allows up to {cap:,} words per document. "
|
| 244 |
"A bigger plan handles longer documents.", title="This document is a bit long")
|
|
@@ -253,8 +267,8 @@ def check_paste(user, text):
|
|
| 253 |
n = len((text or "").split())
|
| 254 |
if n > st["paste_words"]:
|
| 255 |
if st["key"] in ("guest", "free"):
|
| 256 |
-
raise A.Upsell(f"Free pastes are up to {st['paste_words']} words (yours: {n:,}). The
|
| 257 |
-
f"{PLANS['
|
| 258 |
title="Your text is longer than the free limit")
|
| 259 |
raise A.Upsell(f"Your plan allows {st['paste_words']:,} words per paste (yours: {n:,}). Upload a .docx for "
|
| 260 |
"longer text.", kind="limit", title="Your text is a bit long")
|
|
@@ -293,22 +307,17 @@ def doc_words(path):
|
|
| 293 |
|
| 294 |
# ---------------------------------------------------------------- consume (after a successful run)
|
| 295 |
def consume_doc(user):
|
| 296 |
-
"""
|
| 297 |
-
allowance uses one extra-document credit. Free plan: credits first, then the free trial."""
|
| 298 |
st = state(user)
|
| 299 |
if st["admin"] or not user:
|
| 300 |
return
|
| 301 |
e = user["email"]
|
| 302 |
-
if st["key"] in PAID:
|
| 303 |
-
if st["docs_day"] and st["used_today"] > st["docs_day"] and st["credits"] > 0:
|
| 304 |
-
with _db() as con:
|
| 305 |
-
con.execute("UPDATE users SET doc_credits=MAX(0, doc_credits-1) WHERE email=?", (e,))
|
| 306 |
-
A.request_sync()
|
| 307 |
-
return
|
| 308 |
with _db() as con:
|
| 309 |
-
if st["
|
| 310 |
-
con.execute("UPDATE users SET
|
| 311 |
-
|
|
|
|
|
|
|
| 312 |
con.execute("UPDATE users SET trial_used=COALESCE(trial_used,0)+1 WHERE email=?", (e,))
|
| 313 |
A.request_sync()
|
| 314 |
|
|
@@ -344,9 +353,13 @@ def activate_plan(email, plan, months=1, source="admin", amount="", note=""):
|
|
| 344 |
end_t = start_t
|
| 345 |
for _ in range(months):
|
| 346 |
end_t = add_month(end_t)
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
con.execute("INSERT INTO plan_history (email, plan, started, until, event, note) VALUES (?,?,?,?,?,?)",
|
| 351 |
(email, plan, _ts(start_t), _ts(end_t), event, note or source))
|
| 352 |
msg = (f"{email} is now on {PLANS[plan]['name']} ({event}): paid until {_ist_date(_ts(end_t))}. "
|
|
@@ -393,10 +406,10 @@ def add_credits(email, n, source="admin", amount="", note=""):
|
|
| 393 |
if not k:
|
| 394 |
return False, f"No user found with {email}."
|
| 395 |
con.execute("INSERT INTO payments (email, item, amount, source, note) VALUES (?,?,?,?,?)",
|
| 396 |
-
(email, f"{int(n)}
|
| 397 |
con.execute("UPDATE upgrade_requests SET status='done' WHERE email=? AND status='open'", (email,))
|
| 398 |
A.request_sync()
|
| 399 |
-
return True, f"Added {int(n)}
|
| 400 |
|
| 401 |
|
| 402 |
def request_upgrade(user, item):
|
|
@@ -429,19 +442,17 @@ def usage_line(user):
|
|
| 429 |
f"Sign up free for {FREE_TRIAL_DOCS} document uploads.")
|
| 430 |
parts = []
|
| 431 |
if st["key"] in PAID:
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
if n >= d else f"{n}/{d} documents today")
|
| 436 |
-
parts.append(f"{st['name']}: {today} · paid until {_ist_date(st['until'])}, renew by then to keep it")
|
| 437 |
elif st["status"] == "expired":
|
| 438 |
parts.append("Your plan ended (not renewed). Renew on the Pricing tab")
|
| 439 |
elif st["status"] == "terminated":
|
| 440 |
-
parts.append("Your subscription has been ended. Contact us or choose
|
| 441 |
else:
|
| 442 |
parts.append(f"Free plan: {st['trial_left']} of {FREE_TRIAL_DOCS} free documents left")
|
| 443 |
if st["credits"]:
|
| 444 |
-
parts.append(f"{st['credits']}
|
| 445 |
return " · ".join(parts)
|
| 446 |
|
| 447 |
|
|
@@ -450,58 +461,57 @@ def nudge(user, document):
|
|
| 450 |
st = state(user)
|
| 451 |
if st["admin"] or st["key"] in PAID:
|
| 452 |
return ""
|
| 453 |
-
|
|
|
|
|
|
|
| 454 |
if not user:
|
| 455 |
msg = (f"Want to check a whole thesis? <b>Sign up free</b> and upload {FREE_TRIAL_DOCS} Word/PDF documents — "
|
| 456 |
f"layout kept, highlighted PDF report included.")
|
| 457 |
elif st["status"] in ("expired", "terminated"):
|
| 458 |
-
msg = "<b>Your plan has ended.</b>
|
|
|
|
|
|
|
| 459 |
elif st["trial_left"] > 0:
|
| 460 |
-
msg =
|
| 461 |
-
f"{sp['docs_day']} documents a day for {both('student')}/month.")
|
| 462 |
else:
|
| 463 |
-
msg =
|
| 464 |
-
|
| 465 |
-
return f'<div class="cs-nudge">💎 {msg} Open the <b>Pricing</b> tab to upgrade.</div>'
|
| 466 |
|
| 467 |
|
| 468 |
def pricing_html(currency=None):
|
| 469 |
cur = _cur(currency)
|
| 470 |
other = "USD" if cur == "INR" else "INR"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 471 |
|
| 472 |
-
def card(
|
| 473 |
-
p = PLANS[key]
|
| 474 |
-
feats = [per_day_text(p), f"{p['paste_words']:,}-word pastes",
|
| 475 |
-
"Unlimited paste runs" if p["pastes"] >= UNLIMITED else f"{p['pastes']} paste runs a day",
|
| 476 |
-
f"Documents up to {p['doc_words']:,} words", "Smart Humanize + AI Detector + PDF report",
|
| 477 |
-
"AI / similarity report-guided rewriting"]
|
| 478 |
-
if key == "agency":
|
| 479 |
-
feats.append("For consultancies and editing teams")
|
| 480 |
-
if key == "pro":
|
| 481 |
-
feats.append("Priority support")
|
| 482 |
li = "".join(f"<li>{html.escape(f)}</li>" for f in feats)
|
| 483 |
-
tag = '<div class="cs-pop">
|
| 484 |
return (f'<div class="cs-price{" cs-featured" if featured else ""}">{tag}<div class="cs-pname">'
|
| 485 |
-
f'{html.escape(
|
| 486 |
-
f'<div class="cs-inr">{
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
|
|
|
|
|
|
|
|
|
| 493 |
promo = f'<div class="cs-promo">🎉 {html.escape(PROMO_TEXT)}</div>' if PROMO_TEXT else ""
|
| 494 |
note = (f'<div class="cs-gauge-cap" style="text-align:left">{html.escape(INR_NOTE)}</div>'
|
| 495 |
if INR_NOTE and cur == "INR" else "")
|
| 496 |
-
return (promo + '<div class="cs-prices"
|
| 497 |
-
+
|
| 498 |
|
| 499 |
|
| 500 |
def price_buttons(currency=None):
|
| 501 |
-
"""Labels of the
|
| 502 |
cur = _cur(currency)
|
| 503 |
-
return
|
| 504 |
-
(f"Buy extra documents — {price_of('extra', cur)} each",)
|
| 505 |
|
| 506 |
|
| 507 |
def pay_instructions(user, item):
|
|
@@ -514,7 +524,7 @@ def pay_instructions(user, item):
|
|
| 514 |
request_upgrade(user, item)
|
| 515 |
links = []
|
| 516 |
if PAY_WHATSAPP:
|
| 517 |
-
txt = (f"Hi, I want to buy
|
| 518 |
else f"Hi, I want the {item} for {user['email']}").replace(" ", "%20")
|
| 519 |
links.append(f'<a class="cs-dl" target="_blank" href="https://wa.me/{PAY_WHATSAPP}?text={txt}">'
|
| 520 |
f'💬 Pay via WhatsApp / UPI</a>')
|
|
@@ -560,7 +570,7 @@ def admin_subscriptions(user):
|
|
| 560 |
now = _now()
|
| 561 |
with _db() as con:
|
| 562 |
rows = con.execute("SELECT email, plan, plan_status, plan_started, period_start, plan_until FROM users "
|
| 563 |
-
"WHERE plan IN
|
| 564 |
"ORDER BY plan_until DESC").fetchall()
|
| 565 |
out = []
|
| 566 |
for e, plan, status, started, pstart, until in rows:
|
|
@@ -583,7 +593,7 @@ def admin_revenue_tiles(user):
|
|
| 583 |
A._need_admin(user)
|
| 584 |
now = _ts(_now())
|
| 585 |
with _db() as con:
|
| 586 |
-
rows = con.execute("SELECT plan, COUNT(*) FROM users WHERE plan IN
|
| 587 |
"AND plan_until > ? GROUP BY plan", (now,)).fetchall()
|
| 588 |
ended = con.execute("SELECT COUNT(*) FROM users WHERE plan_status='expired'").fetchone()[0]
|
| 589 |
opens = con.execute("SELECT COUNT(*) FROM upgrade_requests WHERE status='open'").fetchone()[0]
|
|
|
|
| 4 |
Who pays what (all paid plans are MONTHLY; ₹ in India, $ for international users)
|
| 5 |
guest : paste only, 300 words, 15 runs/day, max 2 Smart passes
|
| 6 |
free : + FREE_TRIAL_DOCS document runs (lifetime), 25 pastes/day
|
| 7 |
+
single : ₹499 / $6 for one document (a credit that never expires)
|
| 8 |
+
monthly : ₹4,999 / $59 a month · 300 documents per month (unused documents do not carry over)
|
|
|
|
|
|
|
| 9 |
admin : everything free and unlimited, no nudges, never charged
|
| 10 |
|
| 11 |
Subscriptions (users table)
|
|
|
|
| 39 |
|
| 40 |
# docs_day: documents (reports) per day; 0 = unlimited
|
| 41 |
PLANS = {
|
| 42 |
+
"guest": dict(name="Guest", inr=0, usd=0, docs_month=0, docs_day=0, pastes=15, paste_words=300, passes=2, doc_words=0),
|
| 43 |
+
"free": dict(name="Free", inr=0, usd=0, docs_month=0, docs_day=0, pastes=25, paste_words=300, passes=2, doc_words=15000),
|
| 44 |
+
# the monthly plan; "student" / "agency" / "pro" are earlier plan names kept so existing subscriptions still work
|
| 45 |
+
"monthly": dict(name="Monthly", inr=int(os.getenv("PRICE_MONTHLY", "4999")),
|
| 46 |
+
usd=float(os.getenv("PRICE_MONTHLY_USD", "59")), docs_month=int(os.getenv("MONTHLY_DOCS", "300")),
|
| 47 |
+
docs_day=0, pastes=UNLIMITED, paste_words=5000, passes=4, doc_words=150000),
|
| 48 |
+
"admin": dict(name="Admin", inr=0, usd=0, docs_month=0, docs_day=0, pastes=UNLIMITED, paste_words=20000, passes=4,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
doc_words=UNLIMITED),
|
| 50 |
}
|
| 51 |
+
for _old in ("student", "agency", "pro"): # legacy plan names -> the monthly plan
|
| 52 |
+
PLANS[_old] = PLANS["monthly"]
|
| 53 |
+
PAID = ("monthly", "student", "agency", "pro")
|
| 54 |
+
_PAID_SQL = "('monthly','student','agency','pro')"
|
| 55 |
+
# Single document: one document credit (bought alone, or after the monthly 300 are used); never expires.
|
| 56 |
+
SINGLE = dict(name="Single document", inr=int(os.getenv("PRICE_SINGLE", "499")),
|
| 57 |
+
usd=float(os.getenv("PRICE_SINGLE_USD", "6")))
|
| 58 |
+
EXTRA_DOC = SINGLE # older name
|
| 59 |
INR_NOTE = os.getenv("PRICE_INR_NOTE", "") # e.g. "Prices include 18% GST" / "+18% GST"
|
| 60 |
# Pricing page currency: INR for India (₹), USD for international visitors ($). Visitors switch on the Pricing tab.
|
| 61 |
DEFAULT_CURRENCY = os.getenv("PRICE_CURRENCY", "INR").strip().upper()
|
|
|
|
| 74 |
|
| 75 |
|
| 76 |
def price_of(item, cur="INR"):
|
| 77 |
+
p = SINGLE if item in ("single", "extra") else PLANS[item]
|
| 78 |
return money(p["usd"] if cur == "USD" else p["inr"], cur)
|
| 79 |
|
| 80 |
|
|
|
|
| 86 |
|
| 87 |
|
| 88 |
def per_day_text(p):
|
| 89 |
+
return f"{p['docs_month']} documents per month"
|
| 90 |
# ---------------------------------------------------------------- storage
|
| 91 |
def _db():
|
| 92 |
con = A._db()
|
|
|
|
| 96 |
("trial_used", "INTEGER DEFAULT 0"), ("plan_started", "TEXT"),
|
| 97 |
("plan_status", "TEXT DEFAULT 'free'")):
|
| 98 |
if c not in cols:
|
| 99 |
+
try:
|
| 100 |
+
con.execute(f"ALTER TABLE users ADD COLUMN {c} {ddl}")
|
| 101 |
+
except Exception as err: # added at the same moment by another thread
|
| 102 |
+
if "duplicate column" not in str(err).lower():
|
| 103 |
+
raise
|
| 104 |
con.execute("CREATE TABLE IF NOT EXISTS payments (id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
| 105 |
"ts TEXT DEFAULT CURRENT_TIMESTAMP, email TEXT, item TEXT, amount TEXT, source TEXT, note TEXT)")
|
| 106 |
con.execute("CREATE TABLE IF NOT EXISTS upgrade_requests (id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
|
|
|
| 145 |
"""Switch off every paid plan whose month ended without a renewal. Returns the emails switched off."""
|
| 146 |
now = _ts(_now())
|
| 147 |
with _db() as con:
|
| 148 |
+
q = ("SELECT email, plan, plan_started, plan_until FROM users WHERE plan IN " + _PAID_SQL + " "
|
| 149 |
"AND plan_until IS NOT NULL AND plan_until <= ?")
|
| 150 |
rows = con.execute(q + (" AND email=?" if email else ""), (now, email) if email else (now,)).fetchall()
|
| 151 |
for e, plan, started, until in rows:
|
|
|
|
| 177 |
|
| 178 |
|
| 179 |
# ---------------------------------------------------------------- state
|
| 180 |
+
def _month_window(pstart):
|
| 181 |
+
"""Start of the current subscription month (period_start moved forward month by month) -> (start_ts, changed)."""
|
| 182 |
+
t = _parse(pstart) if pstart else 0
|
| 183 |
+
if not t:
|
| 184 |
+
return _now(), True
|
| 185 |
+
changed = False
|
| 186 |
+
while add_month(t) <= _now():
|
| 187 |
+
t, changed = add_month(t), True
|
| 188 |
+
return t, changed
|
| 189 |
+
|
| 190 |
+
|
| 191 |
def state(user):
|
| 192 |
"""Everything the UI and the checks need about a user's plan."""
|
| 193 |
if A.is_admin(user):
|
| 194 |
+
return dict(key="admin", **PLANS["admin"], until=None, started=None, used=0, used_today=0, credits=0,
|
| 195 |
+
trial_left=0, paid=True, admin=True, status="admin", month_end=None)
|
| 196 |
if not user:
|
| 197 |
+
return dict(key="guest", **PLANS["guest"], until=None, started=None, used=0, used_today=0, credits=0,
|
| 198 |
+
trial_left=0, paid=False, admin=False, status="guest", month_end=None)
|
| 199 |
with _db() as con:
|
| 200 |
+
row = con.execute("SELECT plan, plan_until, plan_started, doc_credits, trial_used, plan_status, "
|
| 201 |
+
"period_start, period_docs FROM users WHERE email=?", (user["email"],)).fetchone()
|
| 202 |
+
plan, until, started, credits, trial, status, pstart, pdocs = row or ("free", None, None, 0, 0, "free", None, 0)
|
| 203 |
if plan in PAID and until and _parse(until) <= _now():
|
| 204 |
expire_due(user["email"]) # month over, not renewed: switch it off now
|
| 205 |
plan, status = "free", "expired"
|
| 206 |
+
used, month_end = 0, None
|
| 207 |
+
if plan in PAID: # documents used in the current subscription month
|
| 208 |
+
t, changed = _month_window(pstart)
|
| 209 |
+
used = 0 if changed else (pdocs or 0)
|
| 210 |
+
if changed:
|
| 211 |
+
with _db() as con:
|
| 212 |
+
con.execute("UPDATE users SET period_start=?, period_docs=0 WHERE email=?", (_ts(t), user["email"]))
|
| 213 |
+
month_end = _ts(min(add_month(t), _parse(until))) if until else _ts(add_month(t))
|
| 214 |
p = PLANS.get(plan, PLANS["free"])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
return dict(key=plan if plan in PLANS else "free", **p, until=until if plan in PAID else None,
|
| 216 |
+
started=started if plan in PAID else None, used=used, used_today=used, credits=credits or 0,
|
| 217 |
+
trial_left=max(0, FREE_TRIAL_DOCS - (trial or 0)), month_end=month_end,
|
| 218 |
paid=plan in PAID or (credits or 0) > 0, admin=False,
|
| 219 |
status="active" if plan in PAID else (status or "free"))
|
| 220 |
|
| 221 |
|
| 222 |
def docs_available(st):
|
| 223 |
+
"""Documents the user can still run: monthly allowance left + single-document credits (+ free trial)."""
|
| 224 |
if st["key"] in PAID:
|
| 225 |
+
return max(0, st["docs_month"] - st["used"]) + st["credits"]
|
| 226 |
return st["credits"] + st["trial_left"]
|
| 227 |
|
| 228 |
|
|
|
|
| 236 |
if st["admin"]:
|
| 237 |
return
|
| 238 |
if st["key"] in PAID:
|
| 239 |
+
if st["used"] >= st["docs_month"] and st["credits"] <= 0:
|
| 240 |
+
raise A.Upsell(f"You've used all {st['docs_month']} documents of this month's plan. Buy single documents "
|
| 241 |
+
f"at {both('single')} each to continue now; your monthly documents start again on "
|
| 242 |
+
f"{_ist_date(st['month_end'])}.", kind="extra", title="Monthly documents used up")
|
|
|
|
|
|
|
| 243 |
cap = st["doc_words"]
|
| 244 |
else:
|
| 245 |
if docs_available(st) <= 0:
|
| 246 |
+
msg = ("Your plan was not renewed, so it has been switched off. Renew it on the 💎 Pricing tab, or buy a "
|
| 247 |
+
f"single document for {both('single')}." if st["status"] == "expired" else
|
| 248 |
+
"Your subscription has been ended. Please contact us, or choose an option on the 💎 Pricing tab."
|
| 249 |
if st["status"] == "terminated" else
|
| 250 |
+
f"You've used your {FREE_TRIAL_DOCS} free documents. Buy a single document for {both('single')}, "
|
| 251 |
+
f"or take the monthly plan: {PLANS['monthly']['docs_month']} documents for "
|
| 252 |
+
f"{both('monthly')}/month.")
|
| 253 |
raise A.Upsell(msg, title="Your plan has ended" if st["status"] in ("expired", "terminated") else
|
| 254 |
"Loved your free documents?")
|
| 255 |
+
cap = PLANS["free"]["doc_words"] if not st["credits"] else PLANS["monthly"]["doc_words"]
|
| 256 |
if words and words > cap:
|
| 257 |
raise A.Upsell(f"This document has {words:,} words; your plan allows up to {cap:,} words per document. "
|
| 258 |
"A bigger plan handles longer documents.", title="This document is a bit long")
|
|
|
|
| 267 |
n = len((text or "").split())
|
| 268 |
if n > st["paste_words"]:
|
| 269 |
if st["key"] in ("guest", "free"):
|
| 270 |
+
raise A.Upsell(f"Free pastes are up to {st['paste_words']} words (yours: {n:,}). The monthly plan allows "
|
| 271 |
+
f"{PLANS['monthly']['paste_words']:,} words per paste — or upload a .docx.",
|
| 272 |
title="Your text is longer than the free limit")
|
| 273 |
raise A.Upsell(f"Your plan allows {st['paste_words']:,} words per paste (yours: {n:,}). Upload a .docx for "
|
| 274 |
"longer text.", kind="limit", title="Your text is a bit long")
|
|
|
|
| 307 |
|
| 308 |
# ---------------------------------------------------------------- consume (after a successful run)
|
| 309 |
def consume_doc(user):
|
| 310 |
+
"""Monthly plan: its 300 documents first, then single-document credits. Otherwise: credits, then the free trial."""
|
|
|
|
| 311 |
st = state(user)
|
| 312 |
if st["admin"] or not user:
|
| 313 |
return
|
| 314 |
e = user["email"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
with _db() as con:
|
| 316 |
+
if st["key"] in PAID and st["used"] < st["docs_month"]:
|
| 317 |
+
con.execute("UPDATE users SET period_docs=COALESCE(period_docs,0)+1 WHERE email=?", (e,))
|
| 318 |
+
elif st["credits"] > 0:
|
| 319 |
+
con.execute("UPDATE users SET doc_credits=MAX(0, doc_credits-1) WHERE email=?", (e,))
|
| 320 |
+
elif st["key"] not in PAID:
|
| 321 |
con.execute("UPDATE users SET trial_used=COALESCE(trial_used,0)+1 WHERE email=?", (e,))
|
| 322 |
A.request_sync()
|
| 323 |
|
|
|
|
| 353 |
end_t = start_t
|
| 354 |
for _ in range(months):
|
| 355 |
end_t = add_month(end_t)
|
| 356 |
+
if active: # same subscription continues: the monthly count runs on
|
| 357 |
+
con.execute("UPDATE users SET plan=?, plan_until=?, plan_started=?, plan_status='active' WHERE email=?",
|
| 358 |
+
(plan, _ts(end_t), keep_started, email))
|
| 359 |
+
else: # new subscription: a fresh month of documents
|
| 360 |
+
con.execute("UPDATE users SET plan=?, plan_until=?, plan_started=?, period_start=?, period_docs=0, "
|
| 361 |
+
"plan_status='active', trial_used=MAX(COALESCE(trial_used,0), ?) WHERE email=?",
|
| 362 |
+
(plan, _ts(end_t), keep_started, _ts(start_t), FREE_TRIAL_DOCS, email))
|
| 363 |
con.execute("INSERT INTO plan_history (email, plan, started, until, event, note) VALUES (?,?,?,?,?,?)",
|
| 364 |
(email, plan, _ts(start_t), _ts(end_t), event, note or source))
|
| 365 |
msg = (f"{email} is now on {PLANS[plan]['name']} ({event}): paid until {_ist_date(_ts(end_t))}. "
|
|
|
|
| 406 |
if not k:
|
| 407 |
return False, f"No user found with {email}."
|
| 408 |
con.execute("INSERT INTO payments (email, item, amount, source, note) VALUES (?,?,?,?,?)",
|
| 409 |
+
(email, f"{int(n)} single document(s)", amount, source, note))
|
| 410 |
con.execute("UPDATE upgrade_requests SET status='done' WHERE email=? AND status='open'", (email,))
|
| 411 |
A.request_sync()
|
| 412 |
+
return True, f"Added {int(n)} single document(s) to {email}."
|
| 413 |
|
| 414 |
|
| 415 |
def request_upgrade(user, item):
|
|
|
|
| 442 |
f"Sign up free for {FREE_TRIAL_DOCS} document uploads.")
|
| 443 |
parts = []
|
| 444 |
if st["key"] in PAID:
|
| 445 |
+
parts.append(f"Monthly plan: {min(st['used'], st['docs_month'])}/{st['docs_month']} documents this month "
|
| 446 |
+
f"(new month starts {_ist_date(st['month_end'])}) · paid until {_ist_date(st['until'])}, "
|
| 447 |
+
f"renew by then to keep it")
|
|
|
|
|
|
|
| 448 |
elif st["status"] == "expired":
|
| 449 |
parts.append("Your plan ended (not renewed). Renew on the Pricing tab")
|
| 450 |
elif st["status"] == "terminated":
|
| 451 |
+
parts.append("Your subscription has been ended. Contact us or choose an option on the Pricing tab")
|
| 452 |
else:
|
| 453 |
parts.append(f"Free plan: {st['trial_left']} of {FREE_TRIAL_DOCS} free documents left")
|
| 454 |
if st["credits"]:
|
| 455 |
+
parts.append(f"{st['credits']} single document(s) available")
|
| 456 |
return " · ".join(parts)
|
| 457 |
|
| 458 |
|
|
|
|
| 461 |
st = state(user)
|
| 462 |
if st["admin"] or st["key"] in PAID:
|
| 463 |
return ""
|
| 464 |
+
mp = PLANS["monthly"]
|
| 465 |
+
offer = (f"Single document: {both('single')} · Monthly plan: {mp['docs_month']} documents for "
|
| 466 |
+
f"{both('monthly')}/month.")
|
| 467 |
if not user:
|
| 468 |
msg = (f"Want to check a whole thesis? <b>Sign up free</b> and upload {FREE_TRIAL_DOCS} Word/PDF documents — "
|
| 469 |
f"layout kept, highlighted PDF report included.")
|
| 470 |
elif st["status"] in ("expired", "terminated"):
|
| 471 |
+
msg = f"<b>Your plan has ended.</b> {offer}"
|
| 472 |
+
elif st["credits"] > 0:
|
| 473 |
+
return ""
|
| 474 |
elif st["trial_left"] > 0:
|
| 475 |
+
msg = f"<b>{st['trial_left']} of {FREE_TRIAL_DOCS} free documents left.</b> {offer}"
|
|
|
|
| 476 |
else:
|
| 477 |
+
msg = f"<b>Your free documents are used up.</b> {offer}"
|
| 478 |
+
return f'<div class="cs-nudge">💎 {msg} Open the <b>Pricing</b> tab.</div>'
|
|
|
|
| 479 |
|
| 480 |
|
| 481 |
def pricing_html(currency=None):
|
| 482 |
cur = _cur(currency)
|
| 483 |
other = "USD" if cur == "INR" else "INR"
|
| 484 |
+
where = "outside India" if other == "USD" else "in India"
|
| 485 |
+
mp = PLANS["monthly"]
|
| 486 |
+
common = ["Smart Humanize + AI Detector + PDF report", "Rigorous proofread", "AI / similarity report-guided rewriting",
|
| 487 |
+
"Word layout, tables and citations kept"]
|
| 488 |
|
| 489 |
+
def card(title, price, per, alt, feats, featured=False):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
li = "".join(f"<li>{html.escape(f)}</li>" for f in feats)
|
| 491 |
+
tag = '<div class="cs-pop">Best value</div>' if featured else ""
|
| 492 |
return (f'<div class="cs-price{" cs-featured" if featured else ""}">{tag}<div class="cs-pname">'
|
| 493 |
+
f'{html.escape(title)}</div><div class="cs-pval">{price}<small>{per}</small></div>'
|
| 494 |
+
f'<div class="cs-inr">{alt} {where}</div><ul>{li}</ul></div>')
|
| 495 |
+
|
| 496 |
+
single = card("Single document", price_of("single", cur), "/document",
|
| 497 |
+
f"{price_of('single', other)}/document",
|
| 498 |
+
["One document, any length up to 150,000 words", "Pay only when you need it",
|
| 499 |
+
"Credit never expires"] + common)
|
| 500 |
+
monthly = card("Monthly plan", price_of("monthly", cur), "/month", f"{price_of('monthly', other)}/month",
|
| 501 |
+
[f"{mp['docs_month']} documents every month", "Unlimited pasted-text runs",
|
| 502 |
+
f"Documents up to {mp['doc_words']:,} words", "Need more in a month? Add single documents"]
|
| 503 |
+
+ common, featured=True)
|
| 504 |
promo = f'<div class="cs-promo">🎉 {html.escape(PROMO_TEXT)}</div>' if PROMO_TEXT else ""
|
| 505 |
note = (f'<div class="cs-gauge-cap" style="text-align:left">{html.escape(INR_NOTE)}</div>'
|
| 506 |
if INR_NOTE and cur == "INR" else "")
|
| 507 |
+
return (promo + '<div class="cs-prices" style="grid-template-columns:repeat(2,minmax(0,1fr));max-width:860px;'
|
| 508 |
+
'margin:0 auto">' + single + monthly + "</div>" + note)
|
| 509 |
|
| 510 |
|
| 511 |
def price_buttons(currency=None):
|
| 512 |
+
"""Labels of the two buy buttons in the chosen currency."""
|
| 513 |
cur = _cur(currency)
|
| 514 |
+
return (f"Buy 1 document — {price_of('single', cur)}", f"Get the Monthly plan — {price_of('monthly', cur)}/month")
|
|
|
|
| 515 |
|
| 516 |
|
| 517 |
def pay_instructions(user, item):
|
|
|
|
| 524 |
request_upgrade(user, item)
|
| 525 |
links = []
|
| 526 |
if PAY_WHATSAPP:
|
| 527 |
+
txt = (f"Hi, I want to buy single documents for {user['email']}. How many: " if item == "Single document"
|
| 528 |
else f"Hi, I want the {item} for {user['email']}").replace(" ", "%20")
|
| 529 |
links.append(f'<a class="cs-dl" target="_blank" href="https://wa.me/{PAY_WHATSAPP}?text={txt}">'
|
| 530 |
f'💬 Pay via WhatsApp / UPI</a>')
|
|
|
|
| 570 |
now = _now()
|
| 571 |
with _db() as con:
|
| 572 |
rows = con.execute("SELECT email, plan, plan_status, plan_started, period_start, plan_until FROM users "
|
| 573 |
+
"WHERE plan IN " + _PAID_SQL + " OR plan_status IN ('expired','terminated') "
|
| 574 |
"ORDER BY plan_until DESC").fetchall()
|
| 575 |
out = []
|
| 576 |
for e, plan, status, started, pstart, until in rows:
|
|
|
|
| 593 |
A._need_admin(user)
|
| 594 |
now = _ts(_now())
|
| 595 |
with _db() as con:
|
| 596 |
+
rows = con.execute("SELECT plan, COUNT(*) FROM users WHERE plan IN " + _PAID_SQL + " "
|
| 597 |
"AND plan_until > ? GROUP BY plan", (now,)).fetchall()
|
| 598 |
ended = con.execute("SELECT COUNT(*) FROM users WHERE plan_status='expired'").fetchone()[0]
|
| 599 |
opens = con.execute("SELECT COUNT(*) FROM upgrade_requests WHERE status='open'").fetchone()[0]
|