Rasheed commited on
Commit
886fddb
·
verified ·
1 Parent(s): 47dd8ae

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +43 -23
  2. plans.py +293 -132
app.py CHANGED
@@ -132,18 +132,20 @@ def upsell_html(up):
132
  icon = {"upgrade": "💎", "signin": "🔐", "limit": "⏳", "extra": "📄"}.get(kind, "✨")
133
  title = html.escape(getattr(up, "headline", "") or "Heads up")
134
  text = html.escape(getattr(up, "text", "") or str(up))
135
- st = P.PLANS["monthly"]
136
  perks = {
137
- "upgrade": [f"Single document: {P.price_of('single', 'INR')} / {P.price_of('single', 'USD')}",
138
- f"Monthly plan: {st['docs_month']} documents for ₹{st['inr']:,} / ${st['usd']:,.0f}",
139
- "Word layout kept + PDF report", "Rigorous proofread included"],
140
- "signin": [f"{P.FREE_TRIAL_DOCS} documents free", "Word & PDF upload", "Run history & re-downloads",
141
- "No card needed"],
142
- "extra": [f"{P.price_of('single', 'INR')} / {P.price_of('single', 'USD')} per single document",
143
- "Continue straight away", "Single documents never expire",
144
- "Your monthly documents start again next month"],
 
 
145
  }.get(kind, [])
146
- go = {"upgrade": ("💎 See plans", "cs-go-pricing"), "extra": ("📄 Buy single documents", "cs-go-pricing"),
147
  "signin": ("🔐 Sign in / Create free account", "cs-go-account")}.get(kind)
148
  perks_html = "".join(f'<li><span>✓</span>{html.escape(x)}</li>' for x in perks)
149
  btns = (f'<button class="cs-up-go" data-cs-go="{go[1]}">{go[0]}</button>' if go else "") + \
@@ -240,6 +242,8 @@ def _stable_uploads(args):
240
  def _stream(core, args, n, idx, user=None, need_login=False, request=None, tool="", words=None, paste=None,
241
  quota=True):
242
  args = _stable_uploads(args)
 
 
243
  key = _job_key(user, request, tool, args)
244
  job = H.find_job(key)
245
  if job is not None and job.done.is_set() and (job.delivered or "error" in job.box):
@@ -251,7 +255,7 @@ def _stream(core, args, n, idx, user=None, need_login=False, request=None, tool=
251
  if need_login:
252
  A._require(user)
253
  if quota:
254
- P.check_doc(user, words) # plan quota / credits / free trial + word cap
255
  if quota:
256
  A.check_limit(user, request, need_login)
257
  except A.Upsell as up: # plan / sign-in / limit -> friendly pop-up, not an error
@@ -271,7 +275,7 @@ def _stream(core, args, n, idx, user=None, need_login=False, request=None, tool=
271
  if quota:
272
  A.add_usage(user, request, need_login)
273
  if need_login and quota:
274
- P.consume_doc(user)
275
  j.extra["mailed"] = M.send_result(user, tool, meta)
276
  b_, a_ = meta.get("before"), meta.get("after")
277
  score = f" — AI score {b_}% → {a_}%" if (b_ is not None and a_ is not None) else ""
@@ -338,6 +342,16 @@ def _smart_result(res, hi, mod, text_mode, rep=None, name=""):
338
  # customers see only the headline; the pass log and engine notes go to the Space logs
339
  print(f"[smart] {name}: AI {b['pct']}% -> {a['pct']}% | " + " | ".join(res["log"]) + f" |{res['note']}", flush=True)
340
  msg = f"✅ **Done** — AI score {b['pct']}% → {a['pct']}%."
 
 
 
 
 
 
 
 
 
 
341
  labels, g_before, g_after = None, b["pct"], a["pct"]
342
  gline = _grammar_line(res.get("note"))
343
  rs = res.get("report")
@@ -477,7 +491,7 @@ def smart_text(text, target, passes, temp, mode, hi, mod, full_ai, vocab, report
477
 
478
 
479
  def smart_docx(path, target, passes, temp, mode, hi, mod, full_ai, vocab, report, rkind, user, request: gr.Request = None):
480
- passes = P.max_passes(user, passes)
481
  yield from _stream(_smart_docx_core, (path, target, passes, temp, mode, hi, mod, bool(full_ai), vocab, report, rkind), 5, 0, user, True, request,
482
  tool="Smart Humanize", words=P.doc_words(path) if path else None)
483
 
@@ -1750,13 +1764,19 @@ with gr.Blocks(title=f"{APP_NAME} · CS Software Solutions", theme=theme, css=CS
1750
  _b = P.price_buttons(P.DEFAULT_CURRENCY)
1751
  with gr.Row():
1752
  pr_single = gr.Button(_b[0])
1753
- pr_monthly = gr.Button(_b[1], variant="primary")
 
 
1754
  pr_pay = gr.HTML()
1755
  gr.HTML('<div class="cs-gauge-cap" style="text-align:left;margin-top:10px">Prices in Indian rupees (₹) '
1756
- 'for India and US dollars ($) for other countries, switch with Currency above. A single document '
1757
- 'is a credit that never expires. The monthly plan runs for one month from the day it starts and '
1758
- 'includes ' + str(P.PLANS["monthly"]["docs_month"]) + ' documents for that month; unused documents '
1759
- 'do not carry over. A plan that is not renewed by its end date is switched off automatically. '
 
 
 
 
1760
  'Results are indicators from our own detector and can differ from other checkers.</div>')
1761
 
1762
  # ============================ ABOUT / CONTACT ============================
@@ -1890,14 +1910,14 @@ with gr.Blocks(title=f"{APP_NAME} · CS Software Solutions", theme=theme, css=CS
1890
  '(refund, misuse, user request); the user goes back to the free plan.</div>')
1891
  with gr.Row():
1892
  ad_p_email = gr.Textbox(label="User email", scale=3)
1893
- ad_p_plan = gr.Dropdown(["monthly"], value="monthly",
1894
  label="Plan", scale=2)
1895
  ad_p_days = gr.Number(value=1, label="Months", precision=0, minimum=1, scale=1)
1896
  ad_p_amount = gr.Textbox(label="Amount received (optional)", scale=2)
1897
  with gr.Row():
1898
  ad_p_activate = gr.Button("✅ Activate plan", variant="primary")
1899
- ad_p_credits_n = gr.Number(value=1, label="Single documents", precision=0)
1900
- ad_p_credits = gr.Button("➕ Add single documents")
1901
  with gr.Row():
1902
  ad_t_reason = gr.Textbox(label="Reason for terminating (optional, sent to the user)",
1903
  scale=3)
@@ -2133,9 +2153,9 @@ with gr.Blocks(title=f"{APP_NAME} · CS Software Solutions", theme=theme, css=CS
2133
  def _switch_currency(c):
2134
  b = P.price_buttons(c)
2135
  return (P.pricing_html(c),) + tuple(gr.update(value=x) for x in b)
2136
- pr_cur.change(_switch_currency, pr_cur, [pr_html, pr_single, pr_monthly], queue=False, **HID)
2137
 
2138
- for b, item in ((pr_single, "Single document"), (pr_monthly, "Monthly plan")):
2139
  b.click(lambda u, item=item: P.pay_instructions(u, item), user_state, pr_pay, **HID)
2140
 
2141
  # admin: plans, credits, requests, payments
 
132
  icon = {"upgrade": "💎", "signin": "🔐", "limit": "⏳", "extra": "📄"}.get(kind, "✨")
133
  title = html.escape(getattr(up, "headline", "") or "Heads up")
134
  text = html.escape(getattr(up, "text", "") or str(up))
135
+ st = P.PLANS["scholar"]
136
  perks = {
137
+ "upgrade": [f"1 credit: {P.price_of('single', 'INR')} / {P.price_of('single', 'USD')} · 5 credits: "
138
+ f"{P.price_of('pack', 'INR')} / {P.price_of('pack', 'USD')}",
139
+ f"Scholar: {st['docs_month']} credits a month for ₹{st['inr']:,} / ${st['usd']:,.0f}",
140
+ f"1 credit = 1 document up to {P.CREDIT_WORDS:,} words",
141
+ "Word layout kept + PDF report + proofread"],
142
+ "signin": [f"Free trial document (up to {P.PLANS['free']['doc_words']:,} words)",
143
+ f"{P.FREE_DETECT_DAY} detector reports a day", "Run history & re-downloads", "No card needed"],
144
+ "extra": [f"Extra credits: {P.price_of('extra', 'INR')} / {P.price_of('extra', 'USD')} each",
145
+ "Continue straight away", f"Bought credits stay valid {P.CREDIT_MONTHS} months",
146
+ "Your monthly credits start again next month"],
147
  }.get(kind, [])
148
+ go = {"upgrade": ("💎 See plans", "cs-go-pricing"), "extra": ("📄 Buy credits", "cs-go-pricing"),
149
  "signin": ("🔐 Sign in / Create free account", "cs-go-account")}.get(kind)
150
  perks_html = "".join(f'<li><span>✓</span>{html.escape(x)}</li>' for x in perks)
151
  btns = (f'<button class="cs-up-go" data-cs-go="{go[1]}">{go[0]}</button>' if go else "") + \
 
242
  def _stream(core, args, n, idx, user=None, need_login=False, request=None, tool="", words=None, paste=None,
243
  quota=True):
244
  args = _stable_uploads(args)
245
+ # the uploaded file's content key (folder name under inputs/): the same file re-run = free re-run
246
+ fkey = next((Path(str(a)).parent.name for a in args if isinstance(a, str) and a.startswith(str(_IN_DIR))), None)
247
  key = _job_key(user, request, tool, args)
248
  job = H.find_job(key)
249
  if job is not None and job.done.is_set() and (job.delivered or "error" in job.box):
 
255
  if need_login:
256
  A._require(user)
257
  if quota:
258
+ P.check_doc(user, words, tool, fkey) # credits / plan / free trial / free re-run + word cap
259
  if quota:
260
  A.check_limit(user, request, need_login)
261
  except A.Upsell as up: # plan / sign-in / limit -> friendly pop-up, not an error
 
275
  if quota:
276
  A.add_usage(user, request, need_login)
277
  if need_login and quota:
278
+ P.consume_doc(user, words, tool, fkey)
279
  j.extra["mailed"] = M.send_result(user, tool, meta)
280
  b_, a_ = meta.get("before"), meta.get("after")
281
  score = f" — AI score {b_}% → {a_}%" if (b_ is not None and a_ is not None) else ""
 
342
  # customers see only the headline; the pass log and engine notes go to the Space logs
343
  print(f"[smart] {name}: AI {b['pct']}% -> {a['pct']}% | " + " | ".join(res["log"]) + f" |{res['note']}", flush=True)
344
  msg = f"✅ **Done** — AI score {b['pct']}% → {a['pct']}%."
345
+ log_ = res.get("log") or []
346
+ n_pass = len({m.group(1) for m in (re.match(r"Pass (\d+): AI score .*rewrote", x) for x in log_) if m})
347
+ why = next((x.split(" — ", 1)[1] for x in reversed(log_) if " — " in x), "")
348
+ locked = max([int(m.group(1)) for m in (re.search(r"\((\d+) skipped", x) for x in log_) if m] or [0])
349
+ if n_pass:
350
+ msg += (f" ({n_pass} pass{'es' if n_pass != 1 else ''}"
351
+ + (f"; {why.replace('stopped: ', 'stopped after ')}" if why else "") + ")")
352
+ if locked:
353
+ msg += (f"\n\n🔒 {locked} flagged sentence(s) could not be edited (citation fields, links, equations or "
354
+ "sub/superscripts inside the sentence) and keep their wording.")
355
  labels, g_before, g_after = None, b["pct"], a["pct"]
356
  gline = _grammar_line(res.get("note"))
357
  rs = res.get("report")
 
491
 
492
 
493
  def smart_docx(path, target, passes, temp, mode, hi, mod, full_ai, vocab, report, rkind, user, request: gr.Request = None):
494
+ passes = P.max_passes(user, passes, document=True)
495
  yield from _stream(_smart_docx_core, (path, target, passes, temp, mode, hi, mod, bool(full_ai), vocab, report, rkind), 5, 0, user, True, request,
496
  tool="Smart Humanize", words=P.doc_words(path) if path else None)
497
 
 
1764
  _b = P.price_buttons(P.DEFAULT_CURRENCY)
1765
  with gr.Row():
1766
  pr_single = gr.Button(_b[0])
1767
+ pr_pack = gr.Button(_b[1])
1768
+ pr_scholar = gr.Button(_b[2], variant="primary")
1769
+ pr_pro = gr.Button(_b[3], variant="primary")
1770
  pr_pay = gr.HTML()
1771
  gr.HTML('<div class="cs-gauge-cap" style="text-align:left;margin-top:10px">Prices in Indian rupees (₹) '
1772
+ 'for India and US dollars ($) for other countries, switch with Currency above. One credit = one '
1773
+ 'document up to ' + f"{P.CREDIT_WORDS:,}" + ' words (longer files use one credit per started '
1774
+ + f"{P.CREDIT_WORDS:,}" + ' words; the count is shown before the run). Bought credits are valid '
1775
+ + str(P.CREDIT_MONTHS) + ' months. Monthly plans run for one month from the day they start; '
1776
+ 'unused monthly credits do not carry over, and a plan not renewed by its end date is switched off '
1777
+ 'automatically. A run that fails costs nothing, and the same file can be re-run '
1778
+ + str(P.FREE_RERUNS) + ' times free within ' + str(P.RERUN_DAYS) + ' days. Refund: a credit is '
1779
+ 'refunded if a run did not lower the AI score on our detector and the free re-runs did not either. '
1780
  'Results are indicators from our own detector and can differ from other checkers.</div>')
1781
 
1782
  # ============================ ABOUT / CONTACT ============================
 
1910
  '(refund, misuse, user request); the user goes back to the free plan.</div>')
1911
  with gr.Row():
1912
  ad_p_email = gr.Textbox(label="User email", scale=3)
1913
+ ad_p_plan = gr.Dropdown(list(P.ADMIN_PLANS), value="scholar",
1914
  label="Plan", scale=2)
1915
  ad_p_days = gr.Number(value=1, label="Months", precision=0, minimum=1, scale=1)
1916
  ad_p_amount = gr.Textbox(label="Amount received (optional)", scale=2)
1917
  with gr.Row():
1918
  ad_p_activate = gr.Button("✅ Activate plan", variant="primary")
1919
+ ad_p_credits_n = gr.Number(value=1, label="Credits (1 single, 5 pack)", precision=0)
1920
+ ad_p_credits = gr.Button("➕ Add credits")
1921
  with gr.Row():
1922
  ad_t_reason = gr.Textbox(label="Reason for terminating (optional, sent to the user)",
1923
  scale=3)
 
2153
  def _switch_currency(c):
2154
  b = P.price_buttons(c)
2155
  return (P.pricing_html(c),) + tuple(gr.update(value=x) for x in b)
2156
+ pr_cur.change(_switch_currency, pr_cur, [pr_html, pr_single, pr_pack, pr_scholar, pr_pro], queue=False, **HID)
2157
 
2158
+ for b, item in zip((pr_single, pr_pack, pr_scholar, pr_pro), P.BUY_ITEMS):
2159
  b.click(lambda u, item=item: P.pay_instructions(u, item), user_state, pr_pay, **HID)
2160
 
2161
  # admin: plans, credits, requests, payments
plans.py CHANGED
@@ -1,28 +1,29 @@
1
  """
2
- Pricing plans, quotas, credits and upgrade nudges.
3
-
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)
12
- plan_started : date the current subscription started (kept across on-time renewals)
13
- period_start : start of the current paid month
14
- plan_until : end of the current paid month (same day next calendar month)
15
- plan_status : active / expired / free
16
- A plan that is not renewed by `plan_until` is switched off automatically (back to the free plan), recorded in
17
- `plan_history`, and the user gets an email. A renewal before the end date adds one month to the end date; a
18
- renewal after it starts a new subscription from today. "A day" for daily limits is the Indian calendar day (IST).
19
-
20
- Money comes in through `activate_plan()` / `add_credits()`:
21
- * today -> the Admin panel (manual activation after a UPI / bank payment)
22
- * Razorpay -> call the same two functions from the payment webhook (see README).
23
  """
24
  import calendar
25
  import html
 
26
  import os
27
  import threading
28
  import time
@@ -31,34 +32,65 @@ import gradio as gr
31
 
32
  import auth as A
33
 
34
- FREE_TRIAL_DOCS = int(os.getenv("FREE_TRIAL_DOCS", "2"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  PAY_EMAIL = os.getenv("PAY_EMAIL", "info@cssoftwaresolutions.tech")
36
- PAY_WHATSAPP = os.getenv("PAY_WHATSAPP", "918886306793") # digits with country code, e.g. 9198xxxxxxxx
37
  PROMO_TEXT = os.getenv("PROMO_TEXT", "")
38
  UNLIMITED = 10 ** 9
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=10, doc_words=150000),
48
- "admin": dict(name="Admin", inr=0, usd=0, docs_month=0, docs_day=0, pastes=UNLIMITED, paste_words=20000, passes=10,
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()
 
62
 
63
 
64
  def _cur(currency):
@@ -67,26 +99,33 @@ def _cur(currency):
67
 
68
 
69
  def money(amount, cur="INR", per=""):
70
- """₹4,999 / $59 ('' -> Free)."""
71
  if not amount:
72
  return "Free"
73
  return f"₹{amount:,.0f}{per}" if cur == "INR" else f"${amount:,.0f}{per}"
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
 
81
  def both(item):
82
- """'₹4,999 ($59)' with the default currency first."""
83
  c1 = _cur(DEFAULT_CURRENCY)
84
  c2 = "USD" if c1 == "INR" else "INR"
85
  return f"{price_of(item, c1)} ({price_of(item, c2)})"
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()
@@ -94,7 +133,7 @@ def _db():
94
  for c, ddl in (("plan", "TEXT DEFAULT 'free'"), ("plan_until", "TEXT"), ("period_start", "TEXT"),
95
  ("period_docs", "INTEGER DEFAULT 0"), ("doc_credits", "INTEGER DEFAULT 0"),
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}")
@@ -108,6 +147,10 @@ def _db():
108
  con.execute("CREATE TABLE IF NOT EXISTS plan_history (id INTEGER PRIMARY KEY AUTOINCREMENT, "
109
  "ts TEXT DEFAULT CURRENT_TIMESTAMP, email TEXT, plan TEXT, started TEXT, until TEXT, "
110
  "event TEXT, note TEXT)")
 
 
 
 
111
  return con
112
 
113
 
@@ -192,14 +235,24 @@ 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"
@@ -214,13 +267,14 @@ def state(user):
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"]
@@ -231,31 +285,93 @@ def _upgrade_msg(reason):
231
  return f"{reason} See the 💎 Pricing tab to upgrade."
232
 
233
 
234
- def check_doc(user, words=None):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  st = state(user)
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")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
 
260
 
261
  def paste_limit(user):
@@ -266,10 +382,10 @@ def check_paste(user, text):
266
  st = state(user)
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")
275
 
@@ -278,12 +394,17 @@ def daily_paste_limit(user):
278
  return state(user)["pastes"]
279
 
280
 
281
- def max_passes(user, requested):
282
- return int(min(int(requested), state(user)["passes"]))
 
 
 
 
 
283
 
284
 
285
  def doc_words(path):
286
- """Quick word count of an uploaded file (for the per-document cap)."""
287
  try:
288
  p = str(path).lower()
289
  if p.endswith(".docx"):
@@ -306,19 +427,37 @@ def doc_words(path):
306
 
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
 
324
 
@@ -399,17 +538,22 @@ def terminate_plan(email, reason="", notify=True, source="admin"):
399
 
400
 
401
  def add_credits(email, n, source="admin", amount="", note=""):
 
 
402
  email = (email or "").strip().lower()
 
 
 
403
  with _db() as con:
404
- k = con.execute("UPDATE users SET doc_credits=COALESCE(doc_credits,0)+? WHERE email=?",
405
- (int(n), email)).rowcount
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):
@@ -438,43 +582,41 @@ def usage_line(user):
438
  if st["admin"]:
439
  return "Admin — everything is free and unlimited."
440
  if not user:
441
- return (f"Guest: {st['paste_words']}-word pastes, {st['pastes']} runs a day. "
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
 
459
  def nudge(user, document):
460
  """Shown under a finished run for guests and free users."""
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
 
@@ -482,60 +624,79 @@ 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):
518
- """Until Razorpay is live: how to pay + the request is logged for the admin."""
519
  if A.is_admin(user):
520
  return '<div class="cs-authmsg cs-ok">You are an admin — every feature is free for you.</div>'
521
  if not user:
522
  return ('<div class="cs-authmsg cs-bad">Please sign in (or create a free account) first, so we can '
523
- 'activate the plan on your account.</div>')
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>')
531
  if PAY_EMAIL:
532
- links.append(f'<a class="cs-dl" href="mailto:{PAY_EMAIL}?subject=Upgrade%20to%20{item.replace(" ", "%20")}'
533
  f'&body=Account:%20{user["email"]}">✉️ Email us to pay</a>')
534
  contact = "".join(links) or (f'<div class="cs-gauge-cap" style="text-align:left">Contact us through '
535
  f'<a href="https://cssoftwaresolutions.in" target="_blank">cssoftwaresolutions.in</a>.</div>')
536
  return (f'<div class="cs-authmsg cs-ok"><b>{html.escape(item)}</b> requested for '
537
  f'<b>{html.escape(user["email"])}</b>. Online card payment is launching shortly; for now pay by UPI or '
538
- f'bank transfer and your plan is switched on the same day.</div>{contact}')
539
 
540
 
541
  # ---------------------------------------------------------------- admin
 
1
  """
2
+ Pricing plans, credits, quotas and upgrade nudges (pricing policy of October 2026).
3
+
4
+ Unit: one CREDIT = one document of up to CREDIT_WORDS (15,000) words; a longer document uses one credit per started
5
+ 15,000 words (a 40,000-word thesis = 3 credits). The number is shown before the run and charged only after a
6
+ successful run (a failed run costs nothing). The same file can be re-run FREE_RERUNS times free within RERUN_DAYS.
7
+
8
+ guest paste only: 300 words a paste, 15 a day, 2 Smart passes
9
+ free 25 pastes a day (300 words); 1 trial document up to 5,000 words; 3 detector files a day
10
+ single ₹199 / $3 one credit, valid 6 months
11
+ pack ₹799 / $12 five credits, valid 6 months
12
+ scholar ₹999 / $14 a month 10 credits a month, pastes up to 1,500 words, unlimited detector
13
+ pro ₹2,999 / $39 a month 40 credits a month, pastes up to 5,000 words, priority
14
+ institution ₹9,999 / $129 a month 150 credits a month (activated by the admin, invoice billing)
15
+ extra ₹149 / $2 one extra credit for monthly-plan users whose month's credits are used
16
+ admin everything free and unlimited
17
+ Journal Finder: free. Journal Formatter: 1 file a month free, included in plans (5 / 20 / 60 a month), ₹99 / $2 per
18
+ file otherwise (shown on the Pricing page; enforced once the Formatter shares the suite's login).
19
+ Monthly plans: 30 days from activation, credits do not carry over, renewal before expiry adds a month on top;
20
+ not renewed by the end date -> switched off automatically (plan_history + email). Prices include GST (INR).
21
+
22
+ Money comes in through `activate_plan()` / `add_credits()` (Admin panel today, Razorpay webhook later).
23
  """
24
  import calendar
25
  import html
26
+ import math
27
  import os
28
  import threading
29
  import time
 
32
 
33
  import auth as A
34
 
35
+
36
+ def _env_i(k, d):
37
+ return int(os.getenv(k, str(d)))
38
+
39
+
40
+ def _env_f(k, d):
41
+ return float(os.getenv(k, str(d)))
42
+
43
+
44
+ FREE_TRIAL_DOCS = _env_i("FREE_TRIAL_DOCS", 1)
45
+ FREE_DETECT_DAY = _env_i("FREE_DETECT_DAY", 3) # detector file reports a day on the free account
46
+ CREDIT_WORDS = _env_i("CREDIT_WORDS", 15000) # words per credit
47
+ CREDIT_MONTHS = _env_i("CREDIT_MONTHS", 6) # bought credits stay valid this many months
48
+ FREE_RERUNS = _env_i("FREE_RERUNS", 2) # free re-runs of the same file ...
49
+ RERUN_DAYS = _env_i("RERUN_DAYS", 7) # ... within this many days
50
  PAY_EMAIL = os.getenv("PAY_EMAIL", "info@cssoftwaresolutions.tech")
51
+ PAY_WHATSAPP = os.getenv("PAY_WHATSAPP", "918886306793")
52
  PROMO_TEXT = os.getenv("PROMO_TEXT", "")
53
  UNLIMITED = 10 ** 9
54
 
55
+ # docs_month = credits per subscription month; format_month = Journal Formatter files a month (display)
56
  PLANS = {
57
+ "guest": dict(name="Guest", inr=0, usd=0, docs_month=0, docs_day=0, pastes=15, paste_words=300, passes=2,
58
+ doc_words=0, format_month=0, priority=False),
59
+ "free": dict(name="Free", inr=0, usd=0, docs_month=0, docs_day=0, pastes=25, paste_words=300, passes=2,
60
+ doc_words=_env_i("FREE_DOC_WORDS", 5000), format_month=1, priority=False),
61
+ "scholar": dict(name="Scholar", inr=_env_i("PRICE_SCHOLAR", 999), usd=_env_f("PRICE_SCHOLAR_USD", 14),
62
+ docs_month=_env_i("SCHOLAR_CREDITS", 10), docs_day=0, pastes=UNLIMITED, paste_words=1500,
63
+ passes=10, doc_words=150000, format_month=5, priority=False),
64
+ "pro": dict(name="Pro", inr=_env_i("PRICE_PRO", 2999), usd=_env_f("PRICE_PRO_USD", 39),
65
+ docs_month=_env_i("PRO_CREDITS", 40), docs_day=0, pastes=UNLIMITED, paste_words=5000, passes=10,
66
+ doc_words=150000, format_month=20, priority=True),
67
+ "institution": dict(name="Institution", inr=_env_i("PRICE_INSTITUTION", 9999),
68
+ usd=_env_f("PRICE_INSTITUTION_USD", 129), docs_month=_env_i("INSTITUTION_CREDITS", 150),
69
+ docs_day=0, pastes=UNLIMITED, paste_words=5000, passes=10, doc_words=150000,
70
+ format_month=60, priority=True),
71
+ # earlier plan (₹4,999 for 300 documents a month): kept so existing subscriptions run to their end date
72
+ "monthly": dict(name="Monthly (earlier plan)", inr=4999, usd=59, docs_month=_env_i("MONTHLY_DOCS", 300),
73
+ docs_day=0, pastes=UNLIMITED, paste_words=5000, passes=10, doc_words=150000, format_month=20,
74
+ priority=True),
75
+ "admin": dict(name="Admin", inr=0, usd=0, docs_month=0, docs_day=0, pastes=UNLIMITED, paste_words=20000,
76
+ passes=10, doc_words=UNLIMITED, format_month=UNLIMITED, priority=True),
77
  }
78
+ for _old in ("student", "agency"): # names from an earlier price list
79
+ PLANS[_old] = PLANS["scholar"]
80
+ PAID = ("scholar", "pro", "institution", "monthly", "student", "agency")
81
+ _PAID_SQL = "('scholar','pro','institution','monthly','student','agency')"
82
+ SELL = ("scholar", "pro") # monthly plans on the Pricing page (institution: on request)
83
+ ADMIN_PLANS = ("scholar", "pro", "institution", "monthly")
84
+
85
+ # credit products
86
+ SINGLE = dict(name="Single credit", inr=_env_i("PRICE_SINGLE", 199), usd=_env_f("PRICE_SINGLE_USD", 3), credits=1)
87
+ PACK = dict(name="Pack of 5 credits", inr=_env_i("PRICE_PACK", 799), usd=_env_f("PRICE_PACK_USD", 12), credits=5)
88
+ EXTRA = dict(name="Extra credit", inr=_env_i("PRICE_EXTRA", 149), usd=_env_f("PRICE_EXTRA_USD", 2), credits=1)
89
+ FORMAT_FILE = dict(name="Journal formatting", inr=_env_i("PRICE_FORMAT", 99), usd=_env_f("PRICE_FORMAT_USD", 2))
90
+ EXTRA_DOC = SINGLE # older name
91
+ INR_NOTE = os.getenv("PRICE_INR_NOTE", "Prices include 18% GST.")
92
  DEFAULT_CURRENCY = os.getenv("PRICE_CURRENCY", "INR").strip().upper()
93
+ _ITEMS = {"single": SINGLE, "pack": PACK, "extra": EXTRA, "format": FORMAT_FILE}
94
 
95
 
96
  def _cur(currency):
 
99
 
100
 
101
  def money(amount, cur="INR", per=""):
102
+ """₹999 / $14 ('' -> Free)."""
103
  if not amount:
104
  return "Free"
105
  return f"₹{amount:,.0f}{per}" if cur == "INR" else f"${amount:,.0f}{per}"
106
 
107
 
108
  def price_of(item, cur="INR"):
109
+ p = _ITEMS.get(item) or PLANS[item]
110
  return money(p["usd"] if cur == "USD" else p["inr"], cur)
111
 
112
 
113
  def both(item):
114
+ """'₹999 ($14)' with the default currency first."""
115
  c1 = _cur(DEFAULT_CURRENCY)
116
  c2 = "USD" if c1 == "INR" else "INR"
117
  return f"{price_of(item, c1)} ({price_of(item, c2)})"
118
 
119
 
120
+ def credits_needed(words):
121
+ """Credits a document of `words` words uses (one per started CREDIT_WORDS words)."""
122
+ return max(1, math.ceil((words or 0) / CREDIT_WORDS))
123
+
124
+
125
  def per_day_text(p):
126
+ return f"{p['docs_month']} credits per month"
127
+
128
+
129
  # ---------------------------------------------------------------- storage
130
  def _db():
131
  con = A._db()
 
133
  for c, ddl in (("plan", "TEXT DEFAULT 'free'"), ("plan_until", "TEXT"), ("period_start", "TEXT"),
134
  ("period_docs", "INTEGER DEFAULT 0"), ("doc_credits", "INTEGER DEFAULT 0"),
135
  ("trial_used", "INTEGER DEFAULT 0"), ("plan_started", "TEXT"),
136
+ ("plan_status", "TEXT DEFAULT 'free'"), ("credits_until", "TEXT")):
137
  if c not in cols:
138
  try:
139
  con.execute(f"ALTER TABLE users ADD COLUMN {c} {ddl}")
 
147
  con.execute("CREATE TABLE IF NOT EXISTS plan_history (id INTEGER PRIMARY KEY AUTOINCREMENT, "
148
  "ts TEXT DEFAULT CURRENT_TIMESTAMP, email TEXT, plan TEXT, started TEXT, until TEXT, "
149
  "event TEXT, note TEXT)")
150
+ con.execute("CREATE TABLE IF NOT EXISTS reruns (email TEXT, fkey TEXT, first_ts TEXT, runs INTEGER DEFAULT 0, "
151
+ "PRIMARY KEY (email, fkey))")
152
+ con.execute("CREATE TABLE IF NOT EXISTS daily_usage (email TEXT, day TEXT, tool TEXT, n INTEGER DEFAULT 0, "
153
+ "PRIMARY KEY (email, day, tool))")
154
  return con
155
 
156
 
 
235
  """Everything the UI and the checks need about a user's plan."""
236
  if A.is_admin(user):
237
  return dict(key="admin", **PLANS["admin"], until=None, started=None, used=0, used_today=0, credits=0,
238
+ credits_until=None,
239
  trial_left=0, paid=True, admin=True, status="admin", month_end=None)
240
  if not user:
241
  return dict(key="guest", **PLANS["guest"], until=None, started=None, used=0, used_today=0, credits=0,
242
+ credits_until=None,
243
  trial_left=0, paid=False, admin=False, status="guest", month_end=None)
244
  with _db() as con:
245
  row = con.execute("SELECT plan, plan_until, plan_started, doc_credits, trial_used, plan_status, "
246
+ "period_start, period_docs, credits_until FROM users WHERE email=?",
247
+ (user["email"],)).fetchone()
248
+ plan, until, started, credits, trial, status, pstart, pdocs, cuntil = \
249
+ row or ("free", None, None, 0, 0, "free", None, 0, None)
250
+ if credits and cuntil and _parse(cuntil) <= _now(): # bought credits are valid CREDIT_MONTHS months
251
+ with _db() as con:
252
+ con.execute("UPDATE users SET doc_credits=0 WHERE email=?", (user["email"],))
253
+ con.execute("INSERT INTO plan_history (email, plan, started, until, event, note) VALUES (?,?,?,?,?,?)",
254
+ (user["email"], "credits", None, cuntil, "credits expired", f"{credits} credit(s)"))
255
+ credits = 0
256
  if plan in PAID and until and _parse(until) <= _now():
257
  expire_due(user["email"]) # month over, not renewed: switch it off now
258
  plan, status = "free", "expired"
 
267
  p = PLANS.get(plan, PLANS["free"])
268
  return dict(key=plan if plan in PLANS else "free", **p, until=until if plan in PAID else None,
269
  started=started if plan in PAID else None, used=used, used_today=used, credits=credits or 0,
270
+ credits_until=cuntil if credits else None,
271
  trial_left=max(0, FREE_TRIAL_DOCS - (trial or 0)), month_end=month_end,
272
  paid=plan in PAID or (credits or 0) > 0, admin=False,
273
  status="active" if plan in PAID else (status or "free"))
274
 
275
 
276
  def docs_available(st):
277
+ """Credits the user can still spend: monthly allowance left + bought credits (+ free trial documents)."""
278
  if st["key"] in PAID:
279
  return max(0, st["docs_month"] - st["used"]) + st["credits"]
280
  return st["credits"] + st["trial_left"]
 
285
  return f"{reason} See the 💎 Pricing tab to upgrade."
286
 
287
 
288
+ def _ist_day():
289
+ return time.strftime("%Y-%m-%d", time.gmtime(_now() + 330 * 60))
290
+
291
+
292
+ def _daily(email, tool):
293
+ with _db() as con:
294
+ r = con.execute("SELECT n FROM daily_usage WHERE email=? AND day=? AND tool=?",
295
+ (email, _ist_day(), tool)).fetchone()
296
+ return r[0] if r else 0
297
+
298
+
299
+ def _daily_add(email, tool):
300
+ with _db() as con:
301
+ k = con.execute("UPDATE daily_usage SET n=n+1 WHERE email=? AND day=? AND tool=?",
302
+ (email, _ist_day(), tool)).rowcount
303
+ if not k:
304
+ con.execute("INSERT INTO daily_usage (email, day, tool, n) VALUES (?,?,?,1)", (email, _ist_day(), tool))
305
+
306
+
307
+ def _free_rerun(email, fkey):
308
+ """True if this file was charged within RERUN_DAYS and still has free re-runs left."""
309
+ if not fkey:
310
+ return False
311
+ with _db() as con:
312
+ r = con.execute("SELECT first_ts, runs FROM reruns WHERE email=? AND fkey=?", (email, fkey)).fetchone()
313
+ return bool(r) and _parse(r[0]) + RERUN_DAYS * 86400 > _now() and (r[1] or 0) < FREE_RERUNS
314
+
315
+
316
+ def _is_detector(tool):
317
+ return "detector" in str(tool or "").lower()
318
+
319
+
320
+ def check_doc(user, words=None, tool="", fkey=None):
321
+ """Before a document run: plan / credits / trial / word cap. Raises A.Upsell with a friendly message."""
322
  st = state(user)
323
  if st["admin"]:
324
  return
325
+ e = (user or {}).get("email")
326
+ if _is_detector(tool): # detector file reports: unlimited on plans, a few a day free
327
+ if st["key"] in PAID or st["credits"] > 0:
328
+ return
329
+ if _daily(e, "detector") >= FREE_DETECT_DAY:
330
+ raise A.Upsell(f"The free account includes {FREE_DETECT_DAY} detector file reports a day. Paste text "
331
+ f"for more checks today, or take the Scholar plan ({both('scholar')}/month) for "
332
+ "unlimited reports.", kind="limit", title="Today's free reports are used")
333
+ return
334
+ if e and _free_rerun(e, fkey):
335
+ return # free re-run of a file charged in the last days
336
+ need = credits_needed(words)
337
+ cap = st["doc_words"] if st["key"] in PAID or st["credits"] > 0 else PLANS["free"]["doc_words"]
338
+ if st["key"] not in PAID and st["credits"] <= 0 and st["trial_left"] > 0:
339
+ if words and words > cap:
340
+ raise A.Upsell(f"This document has {words:,} words; the free trial covers documents up to {cap:,} words. "
341
+ f"A single credit ({both('single')}) covers up to {CREDIT_WORDS:,} words.",
342
+ title="This document is longer than the free trial")
343
+ return
344
+ have = docs_available(st)
345
+ if have < need:
346
+ if st["key"] in PAID:
347
+ raise A.Upsell(f"This document needs {need} credit{'s' if need > 1 else ''} ({words or 0:,} words; one "
348
+ f"credit = {CREDIT_WORDS:,} words) and you have {have} left this month. Add extra credits "
349
+ f"at {both('extra')} each; your monthly credits start again on "
350
+ f"{_ist_date(st['month_end'])}.", kind="extra", title="Monthly credits used up")
351
+ msg = ("Your plan was not renewed, so it has been switched off. Renew it on the 💎 Pricing tab, or buy "
352
+ f"credits ({both('single')} each, {both('pack')} for 5)." if st["status"] == "expired" else
353
+ "Your subscription has been ended. Please contact us, or choose an option on the 💎 Pricing tab."
354
+ if st["status"] == "terminated" else
355
+ f"This document needs {need} credit{'s' if need > 1 else ''} (one credit = {CREDIT_WORDS:,} words). "
356
+ f"Buy a single credit for {both('single')}, a pack of 5 for {both('pack')}, or take Scholar: "
357
+ f"{PLANS['scholar']['docs_month']} credits for {both('scholar')}/month.")
358
+ raise A.Upsell(msg, title="Your plan has ended" if st["status"] in ("expired", "terminated") else
359
+ "Credits needed")
360
+ biggest = st["doc_words"] if st["key"] in PAID else PLANS["scholar"]["doc_words"] # bought credits: any size
361
+ if words and words > biggest:
362
+ raise A.Upsell(f"This document has {words:,} words; the largest document is {biggest:,} words. "
363
+ "Split it into chapters.", title="This document is very long")
364
+
365
+
366
+ def doc_cost_text(user, words, tool=""):
367
+ """'This run uses 2 credits (21,400 words)' for the upload area ('' when not applicable)."""
368
+ if not words or _is_detector(tool):
369
+ return ""
370
+ st = state(user)
371
+ if st["admin"]:
372
+ return ""
373
+ n = credits_needed(words)
374
+ return f"This document ({words:,} words) uses {n} credit{'s' if n > 1 else ''}; you have {docs_available(st)}."
375
 
376
 
377
  def paste_limit(user):
 
382
  st = state(user)
383
  n = len((text or "").split())
384
  if n > st["paste_words"]:
385
+ if st["key"] not in PAID:
386
+ raise A.Upsell(f"Free pastes are up to {st['paste_words']} words (yours: {n:,}). Scholar allows "
387
+ f"{PLANS['scholar']['paste_words']:,} words and Pro {PLANS['pro']['paste_words']:,} per "
388
+ "paste, or upload a .docx with a credit.", title="Your text is longer than the free limit")
389
  raise A.Upsell(f"Your plan allows {st['paste_words']:,} words per paste (yours: {n:,}). Upload a .docx for "
390
  "longer text.", kind="limit", title="Your text is a bit long")
391
 
 
394
  return state(user)["pastes"]
395
 
396
 
397
+ def max_passes(user, requested, document=False):
398
+ """Smart Humanize passes: the plan's limit; the free trial document gets the full automatic passes."""
399
+ st = state(user)
400
+ cap = st["passes"]
401
+ if document and (st["credits"] > 0 or (st["key"] == "free" and st["trial_left"] > 0)):
402
+ cap = 10
403
+ return int(min(int(requested), cap))
404
 
405
 
406
  def doc_words(path):
407
+ """Quick word count of an uploaded file (for credits and the word cap)."""
408
  try:
409
  p = str(path).lower()
410
  if p.endswith(".docx"):
 
427
 
428
 
429
  # ---------------------------------------------------------------- consume (after a successful run)
430
+ def consume_doc(user, words=None, tool="", fkey=None):
431
+ """Charge a successful document run: free re-run if allowed; detector files only count on the free account;
432
+ otherwise credits_needed(words) from the month's allowance, then bought credits, then the free trial."""
433
  st = state(user)
434
  if st["admin"] or not user:
435
  return
436
  e = user["email"]
437
+ if _is_detector(tool):
438
+ if st["key"] not in PAID and st["credits"] <= 0:
439
+ _daily_add(e, "detector")
440
+ return
441
  with _db() as con:
442
+ if fkey:
443
+ r = con.execute("SELECT first_ts, runs FROM reruns WHERE email=? AND fkey=?", (e, fkey)).fetchone()
444
+ if r and _parse(r[0]) + RERUN_DAYS * 86400 > _now() and (r[1] or 0) < FREE_RERUNS:
445
+ con.execute("UPDATE reruns SET runs=COALESCE(runs,0)+1 WHERE email=? AND fkey=?", (e, fkey))
446
+ A.request_sync()
447
+ return
448
+ need = credits_needed(words)
449
+ if st["key"] not in PAID and st["credits"] <= 0 and st["trial_left"] > 0:
450
  con.execute("UPDATE users SET trial_used=COALESCE(trial_used,0)+1 WHERE email=?", (e,))
451
+ else:
452
+ from_month = min(need, max(0, st["docs_month"] - st["used"])) if st["key"] in PAID else 0
453
+ if from_month:
454
+ con.execute("UPDATE users SET period_docs=COALESCE(period_docs,0)+? WHERE email=?", (from_month, e))
455
+ rest = need - from_month
456
+ if rest:
457
+ con.execute("UPDATE users SET doc_credits=MAX(0, COALESCE(doc_credits,0)-?) WHERE email=?", (rest, e))
458
+ if fkey:
459
+ con.execute("INSERT OR REPLACE INTO reruns (email, fkey, first_ts, runs) VALUES (?,?,?,0)",
460
+ (e, fkey, _ts(_now())))
461
  A.request_sync()
462
 
463
 
 
538
 
539
 
540
  def add_credits(email, n, source="admin", amount="", note=""):
541
+ """Add bought credits (single, pack or extra); all of the user's bought credits are then valid CREDIT_MONTHS
542
+ months from today."""
543
  email = (email or "").strip().lower()
544
+ t = _now()
545
+ for _ in range(CREDIT_MONTHS):
546
+ t = add_month(t)
547
  with _db() as con:
548
+ k = con.execute("UPDATE users SET doc_credits=COALESCE(doc_credits,0)+?, credits_until=? WHERE email=?",
549
+ (int(n), _ts(t), email)).rowcount
550
  if not k:
551
  return False, f"No user found with {email}."
552
  con.execute("INSERT INTO payments (email, item, amount, source, note) VALUES (?,?,?,?,?)",
553
+ (email, f"{int(n)} credit(s)", amount, source, note))
554
  con.execute("UPDATE upgrade_requests SET status='done' WHERE email=? AND status='open'", (email,))
555
  A.request_sync()
556
+ return True, f"Added {int(n)} credit(s) to {email}, valid until {_ist_date(_ts(t))}."
557
 
558
 
559
  def request_upgrade(user, item):
 
582
  if st["admin"]:
583
  return "Admin — everything is free and unlimited."
584
  if not user:
585
+ return (f"Guest: {st['paste_words']}-word pastes, {st['pastes']} runs a day. Sign up free for a trial "
586
+ f"document (up to {PLANS['free']['doc_words']:,} words) and {FREE_DETECT_DAY} detector reports a day.")
587
  parts = []
588
  if st["key"] in PAID:
589
+ parts.append(f"{st['name']} plan: {min(st['used'], st['docs_month'])}/{st['docs_month']} credits used this "
590
+ f"month (new month starts {_ist_date(st['month_end'])}) · paid until {_ist_date(st['until'])}, "
591
+ "renew by then to keep it")
592
  elif st["status"] == "expired":
593
  parts.append("Your plan ended (not renewed). Renew on the Pricing tab")
594
  elif st["status"] == "terminated":
595
  parts.append("Your subscription has been ended. Contact us or choose an option on the Pricing tab")
596
  else:
597
+ parts.append(f"Free plan: {st['trial_left']} of {FREE_TRIAL_DOCS} trial document(s) left")
598
  if st["credits"]:
599
+ parts.append(f"{st['credits']} credit(s) available until {_ist_date(st.get('credits_until'))}")
600
+ parts.append(f"1 credit = 1 document up to {CREDIT_WORDS:,} words")
601
  return " · ".join(parts)
602
 
603
 
604
  def nudge(user, document):
605
  """Shown under a finished run for guests and free users."""
606
  st = state(user)
607
+ if st["admin"] or st["key"] in PAID or st["credits"] > 0:
608
  return ""
609
+ offer = (f"1 credit {both('single')} · 5 credits {both('pack')} · Scholar {both('scholar')}/month "
610
+ f"({PLANS['scholar']['docs_month']} credits).")
 
611
  if not user:
612
+ msg = (f"Want to check a whole chapter? <b>Sign up free</b> for a trial document (up to "
613
+ f"{PLANS['free']['doc_words']:,} words): layout kept, highlighted PDF report included.")
614
  elif st["status"] in ("expired", "terminated"):
615
  msg = f"<b>Your plan has ended.</b> {offer}"
 
 
616
  elif st["trial_left"] > 0:
617
+ msg = f"<b>Your free trial document is waiting.</b> {offer}"
618
  else:
619
+ msg = f"<b>Your free trial is used.</b> {offer}"
620
  return f'<div class="cs-nudge">💎 {msg} Open the <b>Pricing</b> tab.</div>'
621
 
622
 
 
624
  cur = _cur(currency)
625
  other = "USD" if cur == "INR" else "INR"
626
  where = "outside India" if other == "USD" else "in India"
627
+ sc, pr = PLANS["scholar"], PLANS["pro"]
628
+ common = ["Smart Humanize + AI detector PDF report", "Rigorous proofread", "Word layout, tables, citations and "
629
+ "equations kept", f"{FREE_RERUNS} free re-runs of the same file within {RERUN_DAYS} days"]
630
 
631
+ def card(title, price, per, alt, feats, featured=False, tag_text="Best value"):
632
  li = "".join(f"<li>{html.escape(f)}</li>" for f in feats)
633
+ tag = f'<div class="cs-pop">{html.escape(tag_text)}</div>' if featured else ""
634
  return (f'<div class="cs-price{" cs-featured" if featured else ""}">{tag}<div class="cs-pname">'
635
  f'{html.escape(title)}</div><div class="cs-pval">{price}<small>{per}</small></div>'
636
  f'<div class="cs-inr">{alt} {where}</div><ul>{li}</ul></div>')
637
 
638
+ cards = [
639
+ card("Single credit", price_of("single", cur), "/credit", f"{price_of('single', other)}/credit",
640
+ [f"1 document up to {CREDIT_WORDS:,} words", f"Valid {CREDIT_MONTHS} months",
641
+ "Journal formatting of the same file included"] + common),
642
+ card("Pack of 5", price_of("pack", cur), "/5 credits", f"{price_of('pack', other)}/5 credits",
643
+ ["5 documents (a full thesis)", f"Valid {CREDIT_MONTHS} months", "Journal formatting of each file "
644
+ "included"] + common),
645
+ card("Scholar", price_of("scholar", cur), "/month", f"{price_of('scholar', other)}/month",
646
+ [f"{sc['docs_month']} credits every month", f"Pasted text up to {sc['paste_words']:,} words, unlimited",
647
+ "Unlimited AI detector reports", f"{sc['format_month']} journal-formatting files a month",
648
+ f"Extra credits {price_of('extra', cur)} each"] + common, featured=True),
649
+ card("Pro", price_of("pro", cur), "/month", f"{price_of('pro', other)}/month",
650
+ [f"{pr['docs_month']} credits every month", f"Pasted text up to {pr['paste_words']:,} words, unlimited",
651
+ "Unlimited AI detector reports", f"{pr['format_month']} journal-formatting files a month",
652
+ "Priority queue", f"Extra credits {price_of('extra', cur)} each"] + common,
653
+ featured=True, tag_text="For heavy users"),
654
+ ]
655
  promo = f'<div class="cs-promo">🎉 {html.escape(PROMO_TEXT)}</div>' if PROMO_TEXT else ""
656
+ extra = (f'<div class="cs-gauge-cap" style="text-align:left;max-width:1100px;margin:10px auto 0">'
657
+ f'<b>Free account:</b> pasted text up to {PLANS["free"]["paste_words"]} words ({PLANS["free"]["pastes"]} a '
658
+ f'day), {FREE_TRIAL_DOCS} trial document up to {PLANS["free"]["doc_words"]:,} words, {FREE_DETECT_DAY} '
659
+ f'detector reports a day, 1 journal-formatting file a month. '
660
+ f'<b>Journal Finder:</b> free for everyone. <b>Journal formatting</b> on its own: '
661
+ f'{price_of("format", cur)} per file. <b>Institutions and agencies:</b> from '
662
+ f'{price_of("institution", cur)}/month for {PLANS["institution"]["docs_month"]} credits, several logins and '
663
+ f'a GST invoice, contact us.'
664
+ + (f' {html.escape(INR_NOTE)}' if INR_NOTE and cur == "INR" else "") + "</div>")
665
+ return (promo + '<div class="cs-prices" style="grid-template-columns:repeat(auto-fit,minmax(220px,1fr));'
666
+ 'max-width:1100px;margin:0 auto">' + "".join(cards) + "</div>" + extra)
667
+
668
+
669
+ BUY_ITEMS = ("Single credit", "Pack of 5 credits", "Scholar plan", "Pro plan")
670
 
671
 
672
  def price_buttons(currency=None):
673
+ """Labels of the four buy buttons in the chosen currency (same order as BUY_ITEMS)."""
674
  cur = _cur(currency)
675
+ return (f"Buy 1 credit — {price_of('single', cur)}", f"Buy 5 credits — {price_of('pack', cur)}",
676
+ f"Scholar — {price_of('scholar', cur)}/month", f"Pro — {price_of('pro', cur)}/month")
677
 
678
 
679
  def pay_instructions(user, item):
680
+ """Until online payment is live: how to pay + the request is logged for the admin."""
681
  if A.is_admin(user):
682
  return '<div class="cs-authmsg cs-ok">You are an admin — every feature is free for you.</div>'
683
  if not user:
684
  return ('<div class="cs-authmsg cs-bad">Please sign in (or create a free account) first, so we can '
685
+ 'activate it on your account.</div>')
686
  request_upgrade(user, item)
687
  links = []
688
  if PAY_WHATSAPP:
689
+ txt = f"Hi, I want to buy: {item} for {user['email']}".replace(" ", "%20")
 
690
  links.append(f'<a class="cs-dl" target="_blank" href="https://wa.me/{PAY_WHATSAPP}?text={txt}">'
691
  f'💬 Pay via WhatsApp / UPI</a>')
692
  if PAY_EMAIL:
693
+ links.append(f'<a class="cs-dl" href="mailto:{PAY_EMAIL}?subject={item.replace(" ", "%20")}'
694
  f'&body=Account:%20{user["email"]}">✉️ Email us to pay</a>')
695
  contact = "".join(links) or (f'<div class="cs-gauge-cap" style="text-align:left">Contact us through '
696
  f'<a href="https://cssoftwaresolutions.in" target="_blank">cssoftwaresolutions.in</a>.</div>')
697
  return (f'<div class="cs-authmsg cs-ok"><b>{html.escape(item)}</b> requested for '
698
  f'<b>{html.escape(user["email"])}</b>. Online card payment is launching shortly; for now pay by UPI or '
699
+ f'bank transfer and it is switched on the same day.</div>{contact}')
700
 
701
 
702
  # ---------------------------------------------------------------- admin