Reporting tools for GrowthVine Capital, a mutual fund distributor (MFD). Two independent report pipelines that both end the same way: Jinja2 → HTML → Playwright (headless Chromium) → PDF.
main.py) — full client report driven by the live InvestWell API.prospect_report/) — a lead-gen funnel: upload a CAS PDF → instant teaser in-browser → email-gated 15-page detailed PDF, deployed as a Flask web app.These share no runtime code path — main.py is a standalone CLI script; prospect_report/ is the deployed web service. They only share the general pattern (Jinja2/Playwright) and some data-fetching idioms.
| Component | Role | Key dependency |
|---|---|---|
main.py |
Monthly report CLI pipeline | InvestWell API, yfinance, Anthropic, Postgres |
mock_main.py / mock_data.py |
Same report, static mock data, no external calls | — |
investwell_api.py |
Thin REST client, JWT auth (15-min token, no refresh loop) | requests |
prospect_report/server.py |
Flask app — upload, teaser, email-gated PDF, mock/preview routes | Flask, gunicorn |
prospect_report/cas_parser.py |
Parses CAMS/KFintech/MF Central CAS PDFs (detailed & holdings-only) | pdfplumber |
prospect_report/prospect_math.py |
Risk/return math: XIRR, Sharpe, Sortino, Beta, Alpha, capture ratios, overlap, tax | numpy, mfapi.in, yfinance |
prospect_report/gv_db.py |
Read-only queries against GrowthVine's Postgres warehouse | psycopg2 |
prospect_report/growthvine_api.py |
Client for GrowthVine's own first-party NAV API | requests |
prospect_report/email_sender.py |
Sends the final PDF via Gmail SMTP | smtplib |
templates/*.html, prospect_report/templates/*.html |
Jinja2 templates rendered to HTML then PDF via Playwright | Jinja2, Playwright |
render.yaml / Procfile |
Deploy config for Render.com | gunicorn |
Overall: prototype / early-access, not production-hardened. It works end-to-end and is deployed, but several choices are fine for a low-traffic lead-gen demo and would break under real production load or scrutiny.
_SESSIONS dict in server.py) — teaser data lives in process memory. gunicorn --workers 1 mitigates this for now, but any restart, deploy, or crash silently drops every in-flight user's session. Doesn't survive a scale-out to >1 worker/dyno either.leads.csv — leads are appended to a flat file on local disk. On Render's free tier this is ephemeral storage — lost on every redeploy/restart. Not concurrency-safe, no backup, no query capability.--workers 1 caps concurrent request handling.threading.Thread(daemon=True)) — fire-and-forget with no retry, no persistence, no dead-letter handling. If the process restarts mid-generation, the lead's report/email is silently lost with only a print() for a trace.investwell_endpoint_tester.py is a manual API smoke-test script, not automated tests. No CI/CD config (.github/ absent) — nothing runs on push/PR.print()/stdout; no error tracking (Sentry etc.), no request/latency metrics, no alerting on failed PDF generation or failed emails.gv_db.py catches DB connection failures and degrades gracefully (documented ~5% coverage caveat for schemeholdings); growthvine_api.py and yfinance calls also fail soft. But there's no circuit breaking, retry/backoff, or timeout tuning beyond ad hoc try/except.InvestwellAPI instance with no refresh — a run longer than 15 minutes will start failing mid-pipeline..env gitignored, Render env vars set via dashboard, no committed credentials found)._SESSIONS and leads.csv with a real datastore (even SQLite on a persistent disk, or the existing Postgres instance) — this is the biggest correctness risk for a lead-gen product where losing a lead is the worst-case outcome.prospect_math.py's calculations (XIRR, overlap, tax) — this is the highest-value, most testable logic in the repo and currently has zero coverage.Scoped to just the Portfolio Review product — prospect_report/ (CAS upload → teaser → email-gated detailed audit). This is the only product from Growthvine Capital SEBI-Ready System Architecture (v1.0, 2026-07-01) that's actually implemented, and it's the one the doc itself flags as highest-stakes: Data Sensitivity: High, Criticality: High, Primary Risks: CAS data leakage, unauthorized report access, external API abuse (doc §4).
The doc defines an exact target workflow for this product (§13.1): Prospect → WAF/ALB → Public API → Consent Store → mfcentral API → Data API → portfolio_review DB → Private S3 → Audit Log, plus 5 required controls. Below is that workflow and those controls checked against the real code.
| Doc's target step | What actually happens |
|---|---|
| WAF/ALB validates request before it reaches the app | Nothing — Flask receives the raw request directly on Render's edge (render.yaml:7). No WAF, no rate limiting, no request-size cap. |
| Consent stored (version + purpose) before any processing | No consent store. upload.html:136-138 is an informational blurb, not a checkbox, and nothing is persisted. |
| CAS fetched live via mfcentral API | Doesn't happen — the doc assumes the app pulls the CAS itself; this app instead has the prospect self-download it from mfcentral.com and upload the PDF (upload.html:118-127). A real architectural difference, not just a missing control — there's no mfcentral integration to audit here at all. |
| Fund details enriched via a separated Data API | prospect_math.py calls gv_db.py directly with app-level Postgres credentials in the same process — functionally similar, but no service boundary, no read-only-credential isolation the doc's split architecture assumes. |
Normalized portfolio data stored in a portfolio_review DB (doc §8.1: High sensitivity, required CAS retention policy + encryption + audit logs) |
No such database exists. Parsed holdings/teaser results live only in the in-process _SESSIONS dict (server.py:42) — no persistence layer at all for this product's core data. |
| Encrypted PDF uploaded to private S3 | PDF written to local disk (server.py:273, main.py:585). No S3, no boto3 in requirements.txt, no encryption-at-rest control. |
| Every processing/report-generation step recorded to an audit log | Only lead capture is logged (_log_lead, server.py:397-411) — timestamp, name, email, health_score, portfolio_value, flags. No event_id/actor/result/correlation_id, no record of parsing, PDF generation, or downloads. |
| Prospect gets back a short-lived download token | Teaser access is gated by a signed Flask session cookie (session["sid"]) — a reasonable de facto token for the teaser page. But the detailed audit PDF is emailed as a raw attachment (email_sender.py), not delivered via any token/link at all. |
server.py:210-214), so the effective retention is zero, which is stricter than the doc's own 7–30 day baseline (§19). No written policy exists, but the behavior already clears the bar./download-teaser-pdf and the emailed detailed report both go out with zero logging of who downloaded what, when.server.py:207-208:else:
error = f"Processing error: {e}"
return render_template("upload.html", error=error)
The raw Python exception string is rendered straight into the user-facing error page. Any pdfplumber/parsing internals, stack-trace fragments, or file-path details leak directly to the browser. This is the one finding here that's a direct, literal violation of an explicit sentence in the target doc — not an inferred gap./mock-preview, /mock-pdf, and /parameter-doc (server.py:225-259, 305-361) are unauthenticated debug/preview routes, live in production with no auth gate, no environment check, and no rate limit. /mock-pdf and /parameter-doc each trigger a full headless-Chromium PDF render on every hit — free, repeatable resource-exhaustion vectors sitting on the public internet, which is exactly the "external API abuse" risk this product is called out for._SESSIONS (server.py:42) never expires and is never cleared — it accumulates for the life of the process. Not a leak to an external party, but it's unbounded retention of parsed financial holdings in memory, contrary to the minimization principle the doc asks for even where no formal policy exists yet./request-audit (server.py:364-392): any freely-typed email address triggers generation and delivery of the other uploader's detailed diagnostics — no OTP/verification loop, so this is a plausible misdirection vector for another prospect's full financial picture. Matches the doc's own incident-runbook category "email misdelivery of report" (§17).server.py:210-214) already exceeds the doc's own retention baseline.gv_db.py's fail-soft handling of DB/coverage gaps matches the "vendor downtime must degrade gracefully" principle (§11), applied correctly to the fund-enrichment step of this same workflow.Against the doc's own 5-step control checklist for this exact product, 2 of 5 pass or half-pass, 3 fail outright — and one failure (the raw exception leaked to the user on parse errors) is a direct violation of an explicit requirement, not an interpretation. The bigger structural gap is that there's no portfolio_review database at all: everything the doc wants governed (retention policy, encryption, audit logs) has no table to attach to yet, because this product currently has no persistence layer beyond an in-memory dict and a CSV file. Fixing the exception-leak and the unverified-email-recipient issue are both same-day fixes; standing up real persistence + audit logging is the actual prerequisite for anything else in this product's SEBI-readiness roadmap.
Four changes came out of reviewing the above, each addressing a concrete production-failure mode rather than a style preference.
Playwright/Chromium isn't wrong for this job — the templates (landscape_template.html, detailed_proposal.html) render charts via Chart.js, so a pure-CSS PDF engine (WeasyPrint, etc.) would silently drop every chart unless they're rebuilt as server-side images first. The actual problem is that a 300MB+ browser binary runs inside the same process that serves live HTTP requests, competing for memory with everything else on a small/free Render instance — that's what fails under load, not the rendering approach itself.
Target: move PDF generation to a separate worker, triggered by a queue instead of an inline function call inside the request handler. Two viable paths:
yfinance scrapes Yahoo's unofficial endpoints and is known to rate-limit or silently block requests from data-center IP ranges (AWS/Render included) — there's no SLA, and a failure here degrades the AI commentary and market-pulse sections of client-facing reports with no warning beyond a print(). Replace with a contracted data source: NSE/BSE official feeds for SENSEX/NIFTY, a paid vendor (Alpha Vantage, Twelve Data) for S&P 500/NASDAQ. This also closes a gap from the SEBI audit's vendor-register requirement (§18) — an unofficial scraper isn't something you can put in a vendor contract.
server.py currently does three unrelated jobs in one process: serve HTML pages, run CAS parsing/math, and generate PDFs. Since the main GrowthVine site is already Node/React, the upload form and teaser results page belong there instead — served with the same branding, session, and auth infrastructure as the rest of the site, calling the Python side purely as a JSON API. Flask (or a successor) keeps no Jinja2-rendered user-facing pages at all.
prospect_math.py, cas_parser.py, gv_db.py, investwell_api.py, and growthvine_api.py currently get import-ed directly into whatever process happens to be serving the request. This is also what the SEBI target doc's own architecture calls for (a dedicated internal "Data API" instance, doc §7.1) and was flagged as missing in the audit above. Pulling this into its own internally-network-only service means:
GV_DB_*) live in exactly one place, scoped read-only, instead of wherever the calling code happens to run.This re-architecture fixes the infrastructure-shaped problems (heavy in-process rendering, unreliable data vendor, mismatched frontend stack, tangled data layer) but doesn't automatically close the SEBI-readiness gaps from the audit above — consent capture, structured audit logging, retention policy, and the unverified-email-recipient issue on report delivery all still need to be built deliberately into the new Node BFF / Data API boundary, not assumed to fall out of the split for free.