Rasheed commited on
Commit
ae7e308
·
verified ·
1 Parent(s): 12739bc

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +21 -24
  2. plans.py +121 -111
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["student"]
132
  perks = {
133
- "upgrade": [f"{st['docs_day']} documents a day", f"{st['paste_words']:,}-word pastes",
134
- "Word layout kept + PDF report", f"from ₹{st['inr']:,} / ${st['usd']:,.0f} a month"],
 
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('extra', 'INR')} / {P.price_of('extra', 'USD')} per extra document",
138
- "Continue straight away, today", "Unused extra documents never expire",
139
- "Daily documents reset at midnight (IST)"],
140
  }.get(kind, [])
141
- go = {"upgrade": ("💎 See plans", "cs-go-pricing"), "extra": ("📄 Buy extra documents", "cs-go-pricing"),
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
- pr_student = gr.Button(_b[0])
1702
- pr_agency = gr.Button(_b[1], variant="primary")
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">All plans are monthly: '
1707
- 'Indian rupees (₹) for India and US dollars ($) for other countries, switch with Currency above. '
1708
- 'A plan runs for one month from the day it starts and must be renewed by its '
1709
- 'end date; a plan that is not renewed is switched off automatically. Daily document limits reset '
1710
- 'at midnight (IST) and unused documents do not carry over. Results are indicators from '
1711
- 'open-source models and may differ from other detectors.</div>')
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(["student", "agency", "pro"], value="student",
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="Extra documents", precision=0)
1851
- ad_p_credits = gr.Button("➕ Add extra documents")
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, pr_student, pr_agency, pr_pro, pr_extra], queue=False,
2088
- **HID)
2089
 
2090
- for b, item in ((pr_student, "Student plan"), (pr_agency, "Agency plan"), (pr_pro, "Pro plan"),
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
- student : ₹4,999 / $59 a month · 5 documents (reports) a day
8
- agency : ₹7,999 / $95 a month · 10 documents (reports) a day
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": dict(name="Student", inr=int(os.getenv("PRICE_STUDENT", "4999")),
47
- usd=float(os.getenv("PRICE_STUDENT_USD", "59")), docs_day=5, pastes=100,
48
- paste_words=2000, passes=4, doc_words=60000),
49
- "agency": dict(name="Agency", inr=int(os.getenv("PRICE_AGENCY", "7999")),
50
- usd=float(os.getenv("PRICE_AGENCY_USD", "95")), docs_day=10, pastes=300,
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
- PAID = ("student", "agency", "pro")
59
- # Extra documents after the daily limit (Student / Agency): bought as credits, used only once today's
60
- # allowance is used, and they never expire.
61
- EXTRA_DOC = dict(name="Extra document", inr=int(os.getenv("EXTRA_DOC_INR", "99")),
62
- usd=float(os.getenv("EXTRA_DOC_USD", "2")))
 
 
 
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 = EXTRA_DOC if item == "extra" else PLANS[item]
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 "Unlimited documents a day" if not p["docs_day"] else f"{p['docs_day']} documents (reports) a day"
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
- con.execute(f"ALTER TABLE users ADD COLUMN {c} {ddl}")
 
 
 
 
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 ('student','agency','pro') "
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, used_today=used_today, credits=credits or 0,
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 today."""
209
  if st["key"] in PAID:
210
- return UNLIMITED if not st["docs_day"] else max(0, st["docs_day"] - st["used_today"])
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["docs_day"] and st["used_today"] >= st["docs_day"] and st["credits"] <= 0:
225
- better = ("Agency (10 a day) or Pro (unlimited)" if st["key"] == "student" else "Pro (unlimited)")
226
- raise A.Upsell(f"You've used all {st['docs_day']} documents of your {st['name']} plan for today. "
227
- f"To continue now, buy extra documents at {both('extra')} each, or wait until "
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 to keep "
234
- "going." if st["status"] == "expired" else
235
- "Your subscription has been ended. Please contact us, or choose a plan on the 💎 Pricing tab."
236
  if st["status"] == "terminated" else
237
- f"You've used your {FREE_TRIAL_DOCS} free documents. Plans start at {both('student')}/month "
238
- f"for {PLANS['student']['docs_day']} documents a day.")
 
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 Student plan allows "
257
- f"{PLANS['student']['paste_words']:,} words per paste — or upload a .docx.",
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
- """Paid plans: counted per day (usage table, already including this document); a document beyond the daily
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["credits"] > 0:
310
- con.execute("UPDATE users SET doc_credits=doc_credits-1 WHERE email=?", (e,))
311
- else:
 
 
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
- con.execute("UPDATE users SET plan=?, plan_until=?, plan_started=?, period_start=?, period_docs=0, "
348
- "plan_status='active', trial_used=MAX(COALESCE(trial_used,0), ?) WHERE email=?",
349
- (plan, _ts(end_t), keep_started, _ts(start_t), FREE_TRIAL_DOCS, email))
 
 
 
 
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)} extra document(s)", amount, source, note))
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)} extra document(s) to {email}."
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
- n, d = st["used_today"], st["docs_day"]
433
- today = (f"{n} documents today (unlimited)" if not d else
434
- f"{d}/{d} documents today, daily limit reached" + (f" (+{n - d} extra)" if n > d else "")
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 a plan on the Pricing tab")
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']} extra document(s) available")
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
- sp = PLANS["student"]
 
 
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> Renew it to continue with your daily documents."
 
 
459
  elif st["trial_left"] > 0:
460
- msg = (f"<b>{st['trial_left']} of {FREE_TRIAL_DOCS} free documents left.</b> Student plan: "
461
- f"{sp['docs_day']} documents a day for {both('student')}/month.")
462
  else:
463
- msg = (f"<b>Your free documents are used up.</b> Student plan: {sp['docs_day']} documents a day for "
464
- f"{both('student')}/month.")
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(key, featured=False):
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">Most popular</div>' if featured else ""
484
  return (f'<div class="cs-price{" cs-featured" if featured else ""}">{tag}<div class="cs-pname">'
485
- f'{html.escape(p["name"])}</div><div class="cs-pval">{price_of(key, cur)}<small>/month</small></div>'
486
- f'<div class="cs-inr">{price_of(key, other)}/month '
487
- f'{"outside India" if other == "USD" else "in India"}</div><ul>{li}</ul></div>')
488
-
489
- extra = (f'<div class="cs-single">📄 <b>Daily limit reached?</b> Student and Agency members can buy extra '
490
- f'documents at <b>{price_of("extra", cur)}</b> each ({price_of("extra", other)} '
491
- f'{"outside India" if other == "USD" else "in India"}). They are used only after your daily documents '
492
- f'are finished, and unused extra documents never expire.</div>')
 
 
 
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">' + card("student") + card("agency", True) + card("pro") + "</div>"
497
- + extra + note)
498
 
499
 
500
  def price_buttons(currency=None):
501
- """Labels of the three buy buttons in the chosen currency."""
502
  cur = _cur(currency)
503
- return tuple(f"Get {PLANS[k]['name']} — {price_of(k, cur)}/month" for k in PAID) + \
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 extra documents for {user['email']}. How many: " if item == "Extra documents"
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 ('student','agency','pro') OR plan_status IN ('expired','terminated') "
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 ('student','pro','agency') "
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]