unknown Claude Opus 5.5 commited on
Commit
662c98f
·
1 Parent(s): c7ef1c5

Pricing for profit on every run: price = 5 x cost after GST and fees (was 4 x), at least Rs 49 (was Rs 10); a document lowered but not to 0% pays half (was free; half is still about 2.5 x cost), one not lowered stays free; Turnitin's set price never below cost + 30%; free tier trimmed (5 pastes a day, trial document up to 1,500 words, guests 3 pastes); Pricing page states the half-price rule; Admin -> Profit tab: net profit after GST, fees, language model, reports and the always-on GPU bill, cost of free use, paid runs needed to cover the fixed bills, runs below cost, profit by tool (7 / 30 / 90 days)

Browse files
app.py CHANGED
@@ -211,6 +211,7 @@ def already_human_html(text_mode):
211
  # Set for a job of a signed-in customer without credits: the job runs, the scores are shown, and the result is
212
  # locked behind a charge priced from what the job used.
213
  PAYG_FOR = __import__("contextvars").ContextVar("pay_per_use_for", default=None)
 
214
  LOCKED_PREVIEW = ('<div class="cs-scores cs-empty"><div>🔒 The text is shown after payment. Your scores are above.'
215
  '</div></div>')
216
 
@@ -506,7 +507,8 @@ def _start_billed_job(key, core, args, user, request, tool, need_login, words, q
506
  meta.get("external_usd", 0), words, meta.get("name") or tool)["charged"]
507
  j.extra["wallet"] = (charged, W.available(who))
508
  else:
509
- charged = W.settle(wallet["ref"], time.time() - t0, llm, words, meta.get("name") or tool)["charged"]
 
510
  j.extra["wallet"] = (charged, W.available(who))
511
  if paid_order: # paid on the page (PhonePe): the order is used up
512
  charged = W.finish_report_order(paid_order, int(meta.get("units_ok", 1)))
@@ -642,7 +644,8 @@ def _smart_result(res, hi, mod, text_mode, rep=None, name=""):
642
  msg += (f"\n\n🔒 {locked} flagged sentence(s) could not be edited (citation fields, links, equations or "
643
  "sub/superscripts inside the sentence) and keep their wording.")
644
  # 0% is the promise: a document that did not reach it is delivered with the sentences still flagged highlighted,
645
- # and it is not charged (no pay-per-use payment, no credit used)
 
646
  short = bool(res.get("zero_goal")) and a["pct"] > 0 and not res.get("report") and not stuck
647
  if short:
648
  msg = (f"⚠️ **AI score {b['pct']}% → {a['pct']}%.** {res.get('left') or 'A few'} sentence(s) still read as "
@@ -696,10 +699,12 @@ def _smart_result(res, hi, mod, text_mode, rep=None, name=""):
696
  out[0] = H.status_done(msg + "\n\n🎁 **No charge for this run**, because the score was not lowered.")
697
  if rep is not None and hasattr(rep, "meta"): # given away, not bought: it earns no free re-runs
698
  rep.meta.update(no_charge=True)
699
- if short and not text_mode:
700
- out[0] = H.status_done(msg + "\n\n🎁 **No charge for this document**, because it did not reach 0%.")
701
- if rep is not None and hasattr(rep, "meta"): # no pay-per-use payment and no credit from the plan
702
- rep.meta.update(no_charge=True)
 
 
703
  if charge: # pay per use: scores now, the document after payment
704
  out = [H.status_done(msg) + pay_popup_html(charge, "smart"), LOCKED_PREVIEW, panel, None, ""]
705
  if text_mode:
@@ -2923,6 +2928,21 @@ with gr.Blocks(title=SITE_TITLE, theme=theme, css=CSS + UI.css() + FND.CSS, js=F
2923
  gr.HTML('<div class="cs-settings-title">🏆 Most active users (30 days)</div>')
2924
  ad_ov_top = gr.Dataframe(headers=["Name", "Email", "Runs", "Documents", "Words", "Last run"],
2925
  interactive=False, wrap=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2926
  # ---------- activity ----------
2927
  with gr.Tab("Activity") as ad_act_tab:
2928
  with gr.Row():
@@ -3136,6 +3156,13 @@ with gr.Blocks(title=SITE_TITLE, theme=theme, css=CSS + UI.css() + FND.CSS, js=F
3136
  tiles, chart, tools, top = DB.admin_overview(u)
3137
  return DB._tiles(P.admin_revenue_tiles(u)) + tiles, chart, tools, top
3138
 
 
 
 
 
 
 
 
3139
  ad_ov_refresh.click(overview, user_state, ov_out, **HID)
3140
  ad_ov_tab.select(overview, user_state, ov_out, **HID)
3141
  admin_tab.select(overview, user_state, ov_out, **HID)
 
211
  # Set for a job of a signed-in customer without credits: the job runs, the scores are shown, and the result is
212
  # locked behind a charge priced from what the job used.
213
  PAYG_FOR = __import__("contextvars").ContextVar("pay_per_use_for", default=None)
214
+ SHORT_PRICE_FACTOR = float(os.getenv("SHORT_PRICE_FACTOR", "0.5")) # lowered, but not to 0%: this share of the price
215
  LOCKED_PREVIEW = ('<div class="cs-scores cs-empty"><div>🔒 The text is shown after payment. Your scores are above.'
216
  '</div></div>')
217
 
 
507
  meta.get("external_usd", 0), words, meta.get("name") or tool)["charged"]
508
  j.extra["wallet"] = (charged, W.available(who))
509
  else:
510
+ charged = W.settle(wallet["ref"], time.time() - t0, llm, words, meta.get("name") or tool,
511
+ factor=meta.get("price_factor", 1.0))["charged"]
512
  j.extra["wallet"] = (charged, W.available(who))
513
  if paid_order: # paid on the page (PhonePe): the order is used up
514
  charged = W.finish_report_order(paid_order, int(meta.get("units_ok", 1)))
 
644
  msg += (f"\n\n🔒 {locked} flagged sentence(s) could not be edited (citation fields, links, equations or "
645
  "sub/superscripts inside the sentence) and keep their wording.")
646
  # 0% is the promise: a document that did not reach it is delivered with the sentences still flagged highlighted,
647
+ # at SHORT_PRICE_FACTOR of the price from the wallet (the owner's rule, 2026-10-28: it was free, which lost money
648
+ # on every such run)
649
  short = bool(res.get("zero_goal")) and a["pct"] > 0 and not res.get("report") and not stuck
650
  if short:
651
  msg = (f"⚠️ **AI score {b['pct']}% → {a['pct']}%.** {res.get('left') or 'A few'} sentence(s) still read as "
 
699
  out[0] = H.status_done(msg + "\n\n🎁 **No charge for this run**, because the score was not lowered.")
700
  if rep is not None and hasattr(rep, "meta"): # given away, not bought: it earns no free re-runs
701
  rep.meta.update(no_charge=True)
702
+ if short: # lowered, not to 0%: half price (still above its cost)
703
+ if rep is not None and hasattr(rep, "meta"):
704
+ rep.meta.update(price_factor=SHORT_PRICE_FACTOR)
705
+ if S.G.WALLET_CAP.get() is not None: # a wallet run (credits and plans are used as usual)
706
+ out[0] = H.status_done(msg + f"\n\n💳 **Charged at {SHORT_PRICE_FACTOR:.0%} of the price**, because it "
707
+ "did not reach 0%.")
708
  if charge: # pay per use: scores now, the document after payment
709
  out = [H.status_done(msg) + pay_popup_html(charge, "smart"), LOCKED_PREVIEW, panel, None, ""]
710
  if text_mode:
 
2928
  gr.HTML('<div class="cs-settings-title">🏆 Most active users (30 days)</div>')
2929
  ad_ov_top = gr.Dataframe(headers=["Name", "Email", "Runs", "Documents", "Words", "Last run"],
2930
  interactive=False, wrap=True)
2931
+ # ---------- profit ----------
2932
+ with gr.Tab("Profit") as ad_pf_tab:
2933
+ with gr.Row():
2934
+ gr.HTML('<div class="cs-auth-title" style="text-align:left">Profit</div>')
2935
+ ad_pf_days = gr.Dropdown([("Last 7 days", 7), ("Last 30 days", 30), ("Last 90 days", 90)],
2936
+ value=30, label="Period", scale=0, min_width=160)
2937
+ ad_pf_refresh = gr.Button("Refresh", size="sm", scale=0, elem_classes="cs-topbtn")
2938
+ ad_pf_tiles = gr.HTML()
2939
+ with gr.Column(elem_classes="cs-card"):
2940
+ gr.HTML('<div class="cs-settings-title">Profit by tool</div>'
2941
+ '<div class="cs-gauge-cap" style="text-align:left">Revenue is what runs were charged '
2942
+ '(wallet and pay-on-page reports). Profit = revenue after GST and the payment fee, '
2943
+ 'minus the language model and bought reports. The GPU bill is a fixed cost, shown '
2944
+ 'above, not split by tool.</div>')
2945
+ ad_pf_table = gr.Dataframe(headers=W.PROFIT_COLS, interactive=False, wrap=True)
2946
  # ---------- activity ----------
2947
  with gr.Tab("Activity") as ad_act_tab:
2948
  with gr.Row():
 
3156
  tiles, chart, tools, top = DB.admin_overview(u)
3157
  return DB._tiles(P.admin_revenue_tiles(u)) + tiles, chart, tools, top
3158
 
3159
+ def profit(u, days):
3160
+ A._need_admin(u)
3161
+ tiles, rows = W.profit_dashboard(int(days or 30))
3162
+ return DB._tiles(tiles), rows
3163
+
3164
+ for ev in (ad_pf_refresh.click, ad_pf_days.change, ad_pf_tab.select):
3165
+ ev(profit, [user_state, ad_pf_days], [ad_pf_tiles, ad_pf_table], **HID)
3166
  ad_ov_refresh.click(overview, user_state, ov_out, **HID)
3167
  ad_ov_tab.select(overview, user_state, ov_out, **HID)
3168
  admin_tab.select(overview, user_state, ov_out, **HID)
plans.py CHANGED
@@ -5,8 +5,9 @@ Unit: one CREDIT = one document of up to CREDIT_WORDS (15,000) words; a longer d
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
@@ -55,10 +56,11 @@ UNLIMITED = 10 ** 9
55
 
56
  # docs_month = credits per subscription month; format_month = Journal Formatter files a month (display)
57
  PLANS = {
58
- "guest": dict(name="Guest", inr=0, usd=0, docs_month=0, docs_day=0, pastes=15, paste_words=300, passes=2,
 
59
  doc_words=0, format_month=0, priority=False),
60
- "free": dict(name="Free", inr=0, usd=0, docs_month=0, docs_day=0, pastes=25, paste_words=300, passes=2,
61
- doc_words=_env_i("FREE_DOC_WORDS", 5000), format_month=1, priority=False),
62
  "scholar": dict(name="Scholar", inr=_env_i("PRICE_SCHOLAR", 999), usd=_env_f("PRICE_SCHOLAR_USD", 14),
63
  docs_month=_env_i("SCHOLAR_CREDITS", 10), docs_day=0, pastes=UNLIMITED, paste_words=1500,
64
  passes=10, doc_words=150000, format_month=5, priority=False),
@@ -776,10 +778,10 @@ def pay_instructions(user, item):
776
  PAY_PER_USE = os.getenv("PAY_PER_USE", "1") != "0"
777
  COST_GPU_HOUR_USD = _env_f("COST_GPU_HOUR_USD", 0.40)
778
  COST_IDLE_FACTOR = max(1.0, _env_f("COST_IDLE_FACTOR", 5))
779
- PRICE_MARGIN = _env_f("PRICE_MARGIN", 200) # profit, percent of cost
780
  PRICE_GST = _env_f("PRICE_GST", 18)
781
  PRICE_USD_INR = _env_f("PRICE_USD_INR", 85)
782
- PRICE_MIN_INR = _env_f("PRICE_MIN_INR", 10) # smallest charge (a payment below this is not worth its fee)
783
  PAY_URL = os.getenv("PAY_URL", "").strip()
784
 
785
 
 
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, 3 a day, 2 Smart passes
9
+ free 5 pastes a day (300 words); 1 trial document up to 1,500 words; 3 detector files a day
10
+ (trimmed 2026-10-28 so free use costs at most about Rs 15 a day per account)
11
  single ₹199 / $3 one credit, valid 6 months
12
  pack ₹799 / $12 five credits, valid 6 months
13
  scholar ₹999 / $14 a month 10 credits a month, pastes up to 1,500 words, unlimited detector
 
56
 
57
  # docs_month = credits per subscription month; format_month = Journal Formatter files a month (display)
58
  PLANS = {
59
+ "guest": dict(name="Guest", inr=0, usd=0, docs_month=0, docs_day=0, pastes=_env_i("GUEST_PASTES", 3),
60
+ paste_words=300, passes=2,
61
  doc_words=0, format_month=0, priority=False),
62
+ "free": dict(name="Free", inr=0, usd=0, docs_month=0, docs_day=0, pastes=_env_i("FREE_PASTES", 5),
63
+ paste_words=300, passes=2, doc_words=_env_i("FREE_DOC_WORDS", 1500), format_month=1, priority=False),
64
  "scholar": dict(name="Scholar", inr=_env_i("PRICE_SCHOLAR", 999), usd=_env_f("PRICE_SCHOLAR_USD", 14),
65
  docs_month=_env_i("SCHOLAR_CREDITS", 10), docs_day=0, pastes=UNLIMITED, paste_words=1500,
66
  passes=10, doc_words=150000, format_month=5, priority=False),
 
778
  PAY_PER_USE = os.getenv("PAY_PER_USE", "1") != "0"
779
  COST_GPU_HOUR_USD = _env_f("COST_GPU_HOUR_USD", 0.40)
780
  COST_IDLE_FACTOR = max(1.0, _env_f("COST_IDLE_FACTOR", 5))
781
+ PRICE_MARGIN = _env_f("PRICE_MARGIN", 400) # profit, percent of cost (as the wallet)
782
  PRICE_GST = _env_f("PRICE_GST", 18)
783
  PRICE_USD_INR = _env_f("PRICE_USD_INR", 85)
784
+ PRICE_MIN_INR = _env_f("PRICE_MIN_INR", 49) # smallest charge (a payment below this is not worth its fee)
785
  PAY_URL = os.getenv("PAY_URL", "").strip()
786
 
787
 
tests/test_app_rephrase_anyway.py CHANGED
@@ -136,9 +136,9 @@ def test_a_free_stuck_pay_per_use_run_is_marked_so_it_earns_no_free_reruns(monke
136
  assert 'if not meta.get("no_charge")' in src and src.index('meta.get("no_charge")') < src.index("P.consume_doc")
137
 
138
 
139
- def test_a_document_not_at_zero_percent_is_never_charged(monkeypatch):
140
- """0% is the promise: a document the engine could not bring to 0% is delivered (remaining sentences highlighted)
141
- with no pay-per-use payment and no plan credit; a document at 0% is charged as usual."""
142
  class Rep:
143
  meta = {}
144
 
@@ -151,13 +151,21 @@ def test_a_document_not_at_zero_percent_is_never_charged(monkeypatch):
151
  rep = Rep()
152
  status = app._smart_result(r, 0.8, 0.5, False, rep, "paper.docx")[0]
153
  assert "100% → 12%" in status and "2 sentence(s) still read as AI-written" in status
154
- assert "No charge for this document" in status
155
- assert rep.meta.get("no_charge") is True and not locks # nothing to pay, no credit used
 
 
 
 
 
 
 
 
156
  rep2 = Rep()
157
  rep2.meta = {}
158
  app._smart_result(dict(_res(100, 0, log, new="Changed text here."), zero_goal=True, left=0), 0.8, 0.5, False,
159
  rep2, "paper.docx")
160
- assert len(locks) == 1 and not rep2.meta.get("no_charge") # at 0%: charged as usual
161
 
162
 
163
  def test_a_reconnecting_page_re_attaches_to_the_tools_job_instead_of_starting_again():
 
136
  assert 'if not meta.get("no_charge")' in src and src.index('meta.get("no_charge")') < src.index("P.consume_doc")
137
 
138
 
139
+ def test_a_document_not_at_zero_percent_pays_half(monkeypatch):
140
+ """A document the engine brought down but not to 0% is delivered (remaining sentences highlighted) at half price
141
+ (the owner's rule of 2026-10-28; it used to be free); a document at 0% is charged in full."""
142
  class Rep:
143
  meta = {}
144
 
 
151
  rep = Rep()
152
  status = app._smart_result(r, 0.8, 0.5, False, rep, "paper.docx")[0]
153
  assert "100% → 12%" in status and "2 sentence(s) still read as AI-written" in status
154
+ assert "No charge" not in status
155
+ assert rep.meta.get("price_factor") == 0.5 and not rep.meta.get("no_charge")
156
+ tok = app.S.G.WALLET_CAP.set(1.0) # a wallet run says so on the page
157
+ try:
158
+ rep3 = Rep()
159
+ rep3.meta = {}
160
+ status = app._smart_result(r, 0.8, 0.5, False, rep3, "paper.docx")[0]
161
+ finally:
162
+ app.S.G.WALLET_CAP.reset(tok)
163
+ assert "Charged at 50% of the price" in status
164
  rep2 = Rep()
165
  rep2.meta = {}
166
  app._smart_result(dict(_res(100, 0, log, new="Changed text here."), zero_goal=True, left=0), 0.8, 0.5, False,
167
  rep2, "paper.docx")
168
+ assert len(locks) == 1 and "price_factor" not in rep2.meta # at 0%: charged in full
169
 
170
 
171
  def test_a_reconnecting_page_re_attaches_to_the_tools_job_instead_of_starting_again():
tests/test_dashboard.py CHANGED
@@ -49,7 +49,7 @@ def test_results_waiting_for_payment_are_shown_with_a_pay_link(user, tmp_path):
49
  price = P.usage_price(300, 0.20)
50
  ref = P.create_charge(user["email"], "Smart Humanize", "thesis.docx", 5120, price, str(tmp_path / "t.docx"))
51
  h = DB._pending_html(user["email"])
52
- assert "thesis.docx" in h and ref in h and "₹111" in h and "Pay now" in h and 'target="_blank"' in h
53
  tiles = DB.user_dashboard(user)[0]
54
  assert ref in tiles
55
  P.settle_charge(ref, source="admin")
 
49
  price = P.usage_price(300, 0.20)
50
  ref = P.create_charge(user["email"], "Smart Humanize", "thesis.docx", 5120, price, str(tmp_path / "t.docx"))
51
  h = DB._pending_html(user["email"])
52
+ assert "thesis.docx" in h and ref in h and "₹184" in h and "Pay now" in h and 'target="_blank"' in h
53
  tiles = DB.user_dashboard(user)[0]
54
  assert ref in tiles
55
  P.settle_charge(ref, source="admin")
tests/test_pricing.py CHANGED
@@ -39,9 +39,9 @@ def test_price_is_cost_plus_margin_plus_gst():
39
  assert p["gpu_usd"] == pytest.approx(300 * 0.40 / 3600)
40
  assert p["space_usd"] == pytest.approx(p["gpu_usd"] * 4) # idle share of the always-on Space
41
  assert p["cost_usd"] == pytest.approx(p["gpu_usd"] * 5 + 0.20)
42
- assert p["profit_usd"] == pytest.approx(2 * p["cost_usd"]) # 200% margin: price before tax = 3 x cost
43
- assert p["gst_usd"] == pytest.approx(0.18 * 3 * p["cost_usd"])
44
- assert p["inr"] == 111 # $1.298 x 85, rounded up
45
  assert P.usage_price(1, 0)["inr"] == P.PRICE_MIN_INR # never below the smallest charge
46
 
47
 
@@ -66,7 +66,7 @@ def test_charge_is_locked_until_it_is_settled(tmp_path, monkeypatch):
66
  c = app._payg_lock(out, "Smart Humanize", "paper.docx", 4000, 300, None)
67
  finally:
68
  app.PAYG_FOR.reset(tok)
69
- assert c["status"] == "pending" and c["amount_inr"] == 111 and c["ref"].startswith("CS-")
70
  user = {"email": "payer@example.com"}
71
  status, file, dl = app.claim_paid(user)
72
  assert "not confirmed yet" in status and "Pay now" in status and not isinstance(file, str)
 
39
  assert p["gpu_usd"] == pytest.approx(300 * 0.40 / 3600)
40
  assert p["space_usd"] == pytest.approx(p["gpu_usd"] * 4) # idle share of the always-on Space
41
  assert p["cost_usd"] == pytest.approx(p["gpu_usd"] * 5 + 0.20)
42
+ assert p["profit_usd"] == pytest.approx(4 * p["cost_usd"]) # 400% margin: price before tax = 5 x cost
43
+ assert p["gst_usd"] == pytest.approx(0.18 * 5 * p["cost_usd"])
44
+ assert p["inr"] == 184 # $2.164 x 85, rounded up
45
  assert P.usage_price(1, 0)["inr"] == P.PRICE_MIN_INR # never below the smallest charge
46
 
47
 
 
66
  c = app._payg_lock(out, "Smart Humanize", "paper.docx", 4000, 300, None)
67
  finally:
68
  app.PAYG_FOR.reset(tok)
69
+ assert c["status"] == "pending" and c["amount_inr"] == 184 and c["ref"].startswith("CS-")
70
  user = {"email": "payer@example.com"}
71
  status, file, dl = app.claim_paid(user)
72
  assert "not confirmed yet" in status and "Pay now" in status and not isinstance(file, str)
tests/test_profit_rules.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pricing of 2026-10-28: price = 5 x cost (after GST and fees), at least Rs 49; a document that came down but not to
2
+ 0% pays half (still above its cost); the free tier is small; the admin's profit page adds up."""
3
+ import os
4
+ import tempfile
5
+ import uuid
6
+
7
+ os.environ.setdefault("USERS_DB", os.path.join(tempfile.mkdtemp(), "users.db"))
8
+ os.environ.setdefault("SKIP_MODEL_LOAD", "1")
9
+ import plans as P # noqa: E402
10
+ import wallet as W # noqa: E402
11
+
12
+
13
+ def _held(paise):
14
+ email = f"pf-{uuid.uuid4().hex[:8]}@example.com"
15
+ W.adjust(email, paise, "test money")
16
+ return email, W.reserve(email, paise, "Smart Humanize")
17
+
18
+
19
+ def test_margin_and_minimum():
20
+ assert W.MARGIN >= 400 and W.MIN_INR >= 49
21
+ assert W.price_paise(0) == 4900
22
+ b = W.breakdown(300, 0.40)
23
+ assert b["inr"] - b["gst_inr"] - b["fee_inr"] >= 5 * b["cost_usd"] * W.USD_INR - 0.01
24
+
25
+
26
+ def test_half_price_when_not_at_zero_still_covers_the_cost():
27
+ email, ref = _held(100000)
28
+ full = W.breakdown(300, 0.40)["paise"]
29
+ got = W.settle(ref, 300, 0.40, 3500, "x.docx", factor=0.5)["charged"]
30
+ assert abs(got - full / 2) <= 100
31
+ cost_inr = W.breakdown(300, 0.40)["cost_usd"] * W.USD_INR
32
+ assert got / 100 * W._KEEP > cost_inr # half price is still above the cost
33
+ assert W.balance(email) == 100000 - got
34
+
35
+
36
+ def test_half_price_never_below_the_minimum():
37
+ _, ref = _held(10000)
38
+ assert W.settle(ref, 1, 0.0, 50, "tiny", factor=0.5)["charged"] == int(W.MIN_INR * 100)
39
+
40
+
41
+ def test_turnitin_price_keeps_a_minimum_margin():
42
+ secs, llm = W.usual("turnitin", 0)
43
+ p = W.usual_price("turnitin", 0)
44
+ assert p / 100 * W._KEEP >= (W.gpu_usd(secs) + llm) * W.USD_INR * (1 + W.SET_MIN_MARGIN / 100) - 1
45
+
46
+
47
+ def test_free_tier_is_trimmed():
48
+ assert P.PLANS["free"]["pastes"] <= 5 and P.PLANS["guest"]["pastes"] <= 3
49
+ assert P.PLANS["free"]["doc_words"] <= 1500
50
+
51
+
52
+ def test_profit_dashboard_adds_up():
53
+ tool = f"Test tool {uuid.uuid4().hex[:6]}"
54
+ W.record_cost("a@example.com", tool, 2500, 120, 0.30, 17500, "wallet")
55
+ W.record_cost("b@example.com", tool, 300, 20, 0.04, 0, "free")
56
+ tiles, rows = W.profit_dashboard(7)
57
+ row = next(r for r in rows if r[0] == tool)
58
+ assert row[1:4] == [2, 1, 1] and row[4] == "₹175"
59
+ profit = 175 * W._KEEP - (0.34 * W.USD_INR)
60
+ assert row[6] == f"₹{profit:,.0f}"
61
+ labels = [t[0] for t in tiles]
62
+ assert "Net profit (7d)" in labels and "GPU bill (always on)" in labels and "Cost of free use" in labels
tests/test_result_email.py CHANGED
@@ -90,7 +90,7 @@ def test_document_run_through_the_job_runner_is_mailed(monkeypatch, tmp_path):
90
  rep.meta.update(name="synopsis.docx", words=3539, before=18, after=1, output=str(out), no_charge=True)
91
  return ("done", None, None, str(out), "")
92
 
93
- frames = list(app._stream(core, (str(doc),), 5, 0, user, True, None, tool="Smart Humanize", words=3539))
94
  assert sent and sent[0][0] == email and str(out) in sent[0][1], frames[-1][0][:300]
95
  assert "emailed" in frames[-1][0]
96
 
 
90
  rep.meta.update(name="synopsis.docx", words=3539, before=18, after=1, output=str(out), no_charge=True)
91
  return ("done", None, None, str(out), "")
92
 
93
+ frames = list(app._stream(core, (str(doc),), 5, 0, user, True, None, tool="Smart Humanize", words=1200))
94
  assert sent and sent[0][0] == email and str(out) in sent[0][1], frames[-1][0][:300]
95
  assert "emailed" in frames[-1][0]
96
 
wallet.py CHANGED
@@ -35,12 +35,12 @@ def _f(k, d):
35
 
36
  GPU_HOUR_USD = _f("COST_GPU_HOUR_USD", 0.40) # Hugging Face Nvidia T4 small
37
  IDLE_FACTOR = max(1.0, _f("COST_IDLE_FACTOR", 5)) # each busy GPU second carries this many seconds of the bill
38
- MARGIN = _f("PRICE_MARGIN", 300) # profit, % of cost
39
  GST = _f("PRICE_GST", 18)
40
  FEE = _f("PAY_FEE_PCT", 2.0) # payment gateway fee, % of the amount paid ...
41
  FEE_GST = _f("PAY_FEE_GST", 18) # ... plus GST on that fee
42
  USD_INR = _f("PRICE_USD_INR", 85)
43
- MIN_INR = _f("PRICE_MIN_INR", 10)
44
  HOLD_X = _f("WALLET_HOLD_X", 2.0) # reservation = this x the usual price of the job
45
  TOPUPS = [int(x) for x in os.getenv("WALLET_TOPUPS", "100,250,500,1000").split(",") if x.strip()]
46
  TOPUP_MIN, TOPUP_MAX = int(_f("WALLET_TOPUP_MIN", 50)), int(_f("WALLET_TOPUP_MAX", 20000))
@@ -101,14 +101,15 @@ def usual(kind, words):
101
  MARGINS = {"turnitin": _f("REILAA_MARGIN", MARGIN)}
102
 
103
 
104
- # a set price per run (₹, incl. GST); never below the break-even price, so every run stays in profit
105
  SET_PRICE = {"turnitin": _f("REILAA_PRICE_INR", 550)}
 
106
 
107
 
108
  def usual_price(kind, words):
109
  secs, llm = usual(kind, words)
110
  if SET_PRICE.get(kind):
111
- floor = price_paise(gpu_usd(secs) + llm, 0) # cost + GST + fee, no profit
112
  return max(int(round(SET_PRICE[kind])) * 100, floor)
113
  return price_paise(gpu_usd(secs) + llm, MARGINS.get(kind))
114
 
@@ -198,10 +199,13 @@ def reserve(email, paise, tool=""):
198
  return ref
199
 
200
 
201
- def settle(ref, seconds, llm_usd, words=0, name=""):
202
  """Charge a finished job its actual price (at most what was reserved) and release the rest. Returns the
203
- breakdown with the amount charged."""
 
204
  b = breakdown(seconds, llm_usd)
 
 
205
  with _lock, _db() as con:
206
  row = con.execute("SELECT email, paise, tool, status FROM wallet_holds WHERE ref=?", (ref,)).fetchone()
207
  if not row or row[3] != "active":
@@ -500,6 +504,70 @@ def profit_report(days=30):
500
  jobs=jobs, paid_jobs=paid_jobs, gpu_hours_busy=secs / 3600, breakeven_jobs=breakeven)
501
 
502
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
  # ---------------------------------------------------------------------------------------------------- pricing page
504
  def pricing_html(free_note=""):
505
  """The Pricing page: pay as you go from a prepaid wallet, with the usual price of common jobs."""
@@ -521,7 +589,9 @@ def pricing_html(free_note=""):
521
  f'</div><ul><li>Add {tops} or any amount from {inr(TOPUP_MIN * 100)}: UPI, cards, net banking '
522
  f'(PhonePe)</li><li>Each run is priced by what it actually used (its length and the work it needed)'
523
  f'</li><li>The usual price is shown before a run starts; you are never charged more than it reserved'
524
- f'</li><li>A run that fails or is stopped costs nothing</li><li>Your balance never expires</li>'
 
 
525
  f'<li>Prices include {GST:.0f}% GST</li></ul></div>'
526
  f'<table class="cs-wallet-table" style="width:100%;margin-top:14px;border-collapse:collapse">'
527
  f'<thead><tr><th style="text-align:left">Usual price of a run</th><th style="text-align:left">Length'
 
35
 
36
  GPU_HOUR_USD = _f("COST_GPU_HOUR_USD", 0.40) # Hugging Face Nvidia T4 small
37
  IDLE_FACTOR = max(1.0, _f("COST_IDLE_FACTOR", 5)) # each busy GPU second carries this many seconds of the bill
38
+ MARGIN = _f("PRICE_MARGIN", 400) # profit, % of cost (price = 5 x cost, after GST and fees)
39
  GST = _f("PRICE_GST", 18)
40
  FEE = _f("PAY_FEE_PCT", 2.0) # payment gateway fee, % of the amount paid ...
41
  FEE_GST = _f("PAY_FEE_GST", 18) # ... plus GST on that fee
42
  USD_INR = _f("PRICE_USD_INR", 85)
43
+ MIN_INR = _f("PRICE_MIN_INR", 49) # smallest charge: covers the fee, start-up and after-work
44
  HOLD_X = _f("WALLET_HOLD_X", 2.0) # reservation = this x the usual price of the job
45
  TOPUPS = [int(x) for x in os.getenv("WALLET_TOPUPS", "100,250,500,1000").split(",") if x.strip()]
46
  TOPUP_MIN, TOPUP_MAX = int(_f("WALLET_TOPUP_MIN", 50)), int(_f("WALLET_TOPUP_MAX", 20000))
 
101
  MARGINS = {"turnitin": _f("REILAA_MARGIN", MARGIN)}
102
 
103
 
104
+ # a set price per run (₹, incl. GST); never below cost + SET_MIN_MARGIN %, so every run stays in profit
105
  SET_PRICE = {"turnitin": _f("REILAA_PRICE_INR", 550)}
106
+ SET_MIN_MARGIN = _f("SET_PRICE_MIN_MARGIN", 30)
107
 
108
 
109
  def usual_price(kind, words):
110
  secs, llm = usual(kind, words)
111
  if SET_PRICE.get(kind):
112
+ floor = price_paise(gpu_usd(secs) + llm, SET_MIN_MARGIN) # cost + GST + fee + a minimum profit
113
  return max(int(round(SET_PRICE[kind])) * 100, floor)
114
  return price_paise(gpu_usd(secs) + llm, MARGINS.get(kind))
115
 
 
199
  return ref
200
 
201
 
202
+ def settle(ref, seconds, llm_usd, words=0, name="", factor=1.0):
203
  """Charge a finished job its actual price (at most what was reserved) and release the rest. Returns the
204
+ breakdown with the amount charged. factor < 1: a reduced price (a run that did not reach 0% pays half), never
205
+ below the minimum charge."""
206
  b = breakdown(seconds, llm_usd)
207
+ if factor < 1:
208
+ b["paise"] = max(int(MIN_INR * 100), int(math.ceil(b["paise"] * max(0.0, factor) / 100)) * 100)
209
  with _lock, _db() as con:
210
  row = con.execute("SELECT email, paise, tool, status FROM wallet_holds WHERE ref=?", (ref,)).fetchone()
211
  if not row or row[3] != "active":
 
504
  jobs=jobs, paid_jobs=paid_jobs, gpu_hours_busy=secs / 3600, breakeven_jobs=breakeven)
505
 
506
 
507
+ FIXED_MONTH_INR = _f("COST_FIXED_MONTH_INR", 0) # other monthly bills (domain, storage, mail ...), ₹
508
+ PROFIT_COLS = ["Tool", "Runs", "Paid runs", "Free / plan runs", "Revenue (incl. GST)", "Direct cost",
509
+ "Profit", "Margin"]
510
+
511
+
512
+ def profit_dashboard(days=30):
513
+ """The admin's profit page: (tiles [(label, value)], rows per tool) for the last `days` days, in ₹.
514
+ Revenue = what runs were charged (wallet and pay-on-page reports); out of it go GST and the gateway fee.
515
+ Direct cost = the language model and bought reports (Turnitin) of every run, paid or free. Fixed cost = the
516
+ GPU billed every hour, busy or not, plus COST_FIXED_MONTH_INR."""
517
+ since = f"-{int(days)} days"
518
+ with _db() as con:
519
+ rows = con.execute("SELECT tool, how, COUNT(*), COALESCE(SUM(charged_paise),0), COALESCE(SUM(llm_usd),0), "
520
+ "COALESCE(SUM(seconds),0), COALESCE(SUM(charged_paise > 0),0) FROM job_costs "
521
+ "WHERE ts >= datetime('now', ?) GROUP BY tool, how", (since,)).fetchall()
522
+ losers = con.execute("SELECT COUNT(*) FROM job_costs WHERE ts >= datetime('now', ?) AND charged_paise > 0 "
523
+ "AND charged_paise / 100.0 * ? < llm_usd * ?", (since, _KEEP, USD_INR)).fetchone()[0]
524
+ keep = lambda inr_: inr_ * _KEEP # what stays after GST and the gateway fee
525
+ tools, tot = {}, dict(runs=0, paid=0, free=0, rev=0.0, cost=0.0, free_cost=0.0, secs=0.0)
526
+ for tool, how, n, paise, llm, secs, paid in rows:
527
+ t = tools.setdefault(tool or "—", dict(runs=0, paid=0, free=0, rev=0.0, cost=0.0))
528
+ cost = llm * USD_INR
529
+ free = n - paid
530
+ for d in (t, tot):
531
+ d["runs"] += n
532
+ d["paid"] += paid
533
+ d["free"] += free
534
+ d["rev"] += paise / 100
535
+ d["cost"] += cost
536
+ tot["secs"] += secs
537
+ if how in ("free", "plan") or not paise:
538
+ tot["free_cost"] += cost
539
+ gpu_bill = days * 24 * GPU_HOUR_USD * USD_INR
540
+ fixed = gpu_bill + FIXED_MONTH_INR * days / 30
541
+ gst = tot["rev"] - tot["rev"] / (1 + GST / 100)
542
+ fee = tot["rev"] - gst - keep(tot["rev"])
543
+ gross = keep(tot["rev"]) - tot["cost"] # before the fixed bills
544
+ net = gross - fixed
545
+ per_paid = gross / tot["paid"] if tot["paid"] else 0
546
+ need = math.ceil(fixed / per_paid) if per_paid > 0 else None
547
+ r = lambda v: f"₹{v:,.0f}"
548
+ tiles = [
549
+ (f"Net profit ({days}d)", ("" if net >= 0 else "−") + r(abs(net))),
550
+ ("Revenue (incl. GST)", r(tot["rev"])), ("GST to pay", r(gst)), ("Payment fees", r(fee)),
551
+ ("Language model + reports", r(tot["cost"])), ("GPU bill (always on)", r(gpu_bill)),
552
+ ("Other fixed bills", r(fixed - gpu_bill)),
553
+ ("Profit before fixed bills", r(gross)),
554
+ ("Paid runs", f"{tot['paid']:,}"), ("Free / plan runs", f"{tot['free']:,}"),
555
+ ("Cost of free use", r(tot["free_cost"])),
556
+ ("Avg profit per paid run", r(per_paid) if tot["paid"] else "—"),
557
+ ("Paid runs to cover fixed bills", f"{need:,}" if need else "—"),
558
+ ("Paid runs below cost", f"{losers:,}"),
559
+ ("GPU busy", f"{tot['secs'] / 3600:,.1f} h of {days * 24:,} h"),
560
+ ("Price rule", f"cost × {1 + MARGIN / 100:g}, min {inr(int(MIN_INR * 100))}"),
561
+ ]
562
+ out = []
563
+ for name, t in sorted(tools.items(), key=lambda kv: -kv[1]["rev"]):
564
+ profit = keep(t["rev"]) - t["cost"]
565
+ out.append([name, t["runs"], t["paid"], t["free"], r(t["rev"]), r(t["cost"]),
566
+ ("" if profit >= 0 else "−") + r(abs(profit)),
567
+ f"{100 * profit / t['cost']:.0f}%" if t["cost"] else "—"])
568
+ return tiles, out
569
+
570
+
571
  # ---------------------------------------------------------------------------------------------------- pricing page
572
  def pricing_html(free_note=""):
573
  """The Pricing page: pay as you go from a prepaid wallet, with the usual price of common jobs."""
 
589
  f'</div><ul><li>Add {tops} or any amount from {inr(TOPUP_MIN * 100)}: UPI, cards, net banking '
590
  f'(PhonePe)</li><li>Each run is priced by what it actually used (its length and the work it needed)'
591
  f'</li><li>The usual price is shown before a run starts; you are never charged more than it reserved'
592
+ f'</li><li>A humanized document that comes down but does not reach 0% AI is charged half price; one '
593
+ f'whose score does not come down costs nothing</li>'
594
+ f'<li>A run that fails or is stopped costs nothing</li><li>Your balance never expires</li>'
595
  f'<li>Prices include {GST:.0f}% GST</li></ul></div>'
596
  f'<table class="cs-wallet-table" style="width:100%;margin-top:14px;border-collapse:collapse">'
597
  f'<thead><tr><th style="text-align:left">Usual price of a run</th><th style="text-align:left">Length'