From 483c2cc114a7b9999d96ad7b65f355cd2cb7e5ef Mon Sep 17 00:00:00 2001
From: bolade
Date: Tue, 21 Oct 2025 10:48:58 +0100
Subject: [PATCH] feat: Update investor report generation and HTML template to
include fund details and improve data handling
---
app/__pycache__/main.cpython-312.pyc | Bin 5212 -> 5310 bytes
app/main.py | 8 +-
.../__pycache__/investors.cpython-312.pyc | Bin 23593 -> 23593 bytes
app/routers/report_route.py | 27 ++-
.../__pycache__/llm_parser.cpython-312.pyc | Bin 37401 -> 39736 bytes
.../__pycache__/querying.cpython-312.pyc | Bin 8594 -> 9236 bytes
app/services/llm_parser.py | 122 ++++++++---
app/services/report_gen.py | 198 ++++++++++++------
app/templates/report.html | 69 +++---
investors.db | Bin 27054080 -> 29941760 bytes
10 files changed, 289 insertions(+), 135 deletions(-)
diff --git a/app/__pycache__/main.cpython-312.pyc b/app/__pycache__/main.cpython-312.pyc
index ef94b2959a3913ba0da87acc8ea966b36959d9a2..81d7ae9c88abee887d6213a954e7a0c000542dfd 100644
GIT binary patch
delta 349
zcmYk0ze)o^5XN(t#{4-DM1q1aT`mVHtdw+$qFuyvHgVmXm?h`lvO8yTSXfy20QU$M
z)^doQFQBa==mS_DTG+ZFL7e9MhGBl+_u)UbyeH4Amh8MA{tV@vcUy5T^83n_vvPo1
zMK}?Z!X8u5O-LmKeyp>tS|by+jmseD$|wb#kY=1rCkS8=Co;sapCZ%1WC$Zn`#3?K
zkT^$E1v_n+KPD<FGJS0;O|zdD=wjki;F
oj`PpOvFis;<85KNC{J~*In(Xkuj&3>F1hl=XnU@VS7pO*|Bp+c8kfo54
zSdut7glE%aW8RC5CYxFLT$mZ-HhT${Gihirir?T6{lE;OWz`|Hlj|oC|1&cKlLzAm
NCLr@e*yNYOR{;tLRhIw&
diff --git a/app/main.py b/app/main.py
index 86f67bd..fa6a869 100644
--- a/app/main.py
+++ b/app/main.py
@@ -61,16 +61,18 @@ async def parse_csv(
- Handles AUM, fund sizes, and check sizes as integers
**For companies:**
- - Expected columns: Name, Website, Investor, Final Investor Profile (company profile)
+ - Expected columns: Name, Website, Perplexity Gap Output (or Final Investor Profile)
- 100% manual JSON parsing - no LLM needed
- - Extracts company details, executives, investors, and client categories
- - Automatically links companies to investors in database
+ - **Only extracts:** founded_year and key_executives
+ - **Only updates companies already in the database** (syncs with existing records)
+ - Skips companies not found in the database
**Benefits:**
- Fast processing (5-10s per record)
- Low cost (minimal or no LLM usage)
- Accurate data extraction
- Automatic database persistence
+ - Safe: won't create duplicate companies
"""
# Read uploaded CSV with pandas
content = await file.read()
diff --git a/app/routers/__pycache__/investors.cpython-312.pyc b/app/routers/__pycache__/investors.cpython-312.pyc
index 1bb8a47f00ad1a8c033705fb2444042c8d640e6c..a2ced80967fa58f260745ebbc9d1dac0c312de89 100644
GIT binary patch
delta 22
ccmZ3vgK^~!M()$Ryj%=G(C_tqBez@(08_OF#Q*>R
delta 22
ccmZ3vgK^~!M()$Ryj%=G&{6qzBez@(090%S;{X5v
diff --git a/app/routers/report_route.py b/app/routers/report_route.py
index 8ab7d24..6f422ac 100644
--- a/app/routers/report_route.py
+++ b/app/routers/report_route.py
@@ -52,7 +52,6 @@ async def generate_investor_report(
"website": investor.website,
"headquarters": investor.headquarters,
"aum": investor.aum,
- "geographic_focus": investor.geographic_focus,
"portfolio_highlights": investor.portfolio_highlights or [],
"investment_thesis": investor.investment_thesis or [],
"sectors": [sector.name for sector in investor.sectors],
@@ -65,24 +64,22 @@ async def generate_investor_report(
}
for member in investor.team_members
],
- "check_size_lower": None,
- "check_size_upper": None,
- "investment_stages": [],
+ "funds": [],
}
- # Get check sizes and stages from funds
+ # Get all funds with their data
if investor.funds:
- # Use the first fund's data or aggregate
- fund = investor.funds[0]
- investor_data["check_size_lower"] = fund.check_size_lower
- investor_data["check_size_upper"] = fund.check_size_upper
-
- # Aggregate all investment stages from all funds
- stages = set()
for fund in investor.funds:
- for stage in fund.investment_stages:
- stages.add(stage.name)
- investor_data["investment_stages"] = list(stages)
+ fund_data = {
+ "fund_name": fund.fund_name,
+ "fund_size": fund.fund_size,
+ "check_size_lower": fund.check_size_lower,
+ "check_size_upper": fund.check_size_upper,
+ "geographic_focus": fund.geographic_focus,
+ "investment_stages": [stage.name for stage in fund.investment_stages],
+ "sectors": [sector.name for sector in fund.sectors],
+ }
+ investor_data["funds"].append(fund_data)
# Fetch project data if project_id is provided
project_data = None
diff --git a/app/services/__pycache__/llm_parser.cpython-312.pyc b/app/services/__pycache__/llm_parser.cpython-312.pyc
index 35f2e13aff671e2c87b9195b7fec7b87a9b3e151..3a1306504c842deada9088c4c53934631535b076 100644
GIT binary patch
delta 6326
zcmZ`-dvH_dmA_YS`6bzsY+1G}$u_oSW8(+d#()C^+q`0v5+FdSk##Rdwk)ZiB#d1-
zqUml5Nt#&s(#0etC@Iq_rJaTKv`hXVNoNztHc2DJ-m$8(TPMwKo85GxlI$i+I_;kG
z$uHRS&ggf~cV6H5p66VhyC}K(GfCc?xw%;!Jg4@5cKog9uH>CGW-Fj6h2JN?l+uW?
zRTAQ9zFALwTU3e(Nk}rJq*qEx%ltV3H5%e0NH6gjis#1o1r;3)cc^7V3`yt~FrN%%
zDJP^Msh}vPe?M46AL__55IMA!0&n6_LQK$771PTtMc7$^pgzGd4PH>8h9IY5n}J^b
zikgn~t%yeh@#yH9jcPT~g(L!M)5Lnia=`ga;ZmnMJRu9o1l4d2$B~uc=d280P)x`L
z)r2CT%M6L*Lb8BiiIyztUXo;Z+qfgO95rYJ0uG!V16V4@RoV#=8cB%
zQlJy6kZLbi!37EzF*qsh{P}FL~j)6_da(Mj&X1x4~q}Lp;p8
z)&wz!_=zm^63)05@Kv+R)Ho1=`+_@dVBm{RD`^<#u@ntzQp%1K=0jTg8*WwF-`a
z!|FH|5DDunV;aUldhOEN{5Nh(!Q*YdI#>@Q1@8OLL_fazBuKrP;QRp+bOwS%v>x*Y
z!E3$MKkV}Ppf7SCSls5b2=2EJ@tl457kKPaCK#82-)e^Cb_wZay5(dyyo1fS26y)5
z^@$#ge}Fig%UyZA0qY5e+a<7AZu1jC*wJ@cjyM9&<7n!1d;NF02$n3UpMN|v8lQup
zPS`3E;w2r{58nWV@f?U{4-rqe#L*7xZ7EE15DyGS2!{JS;z^(A48RI4SlKlxiBCw$
zTz-!uC5IVu`BGW_fP)15qprYlvKfql?{m8XDcO+MbTkHBOp*)f+#$VthG!!szR
zq;Ane|5R*o-vcK0Y|;uZvJ=g7oNm$K5ruwS`IHQ3)YD>%J%PFI`I)yC^K&gwSZ)EeT2U9;NmWKjtSwWV{K
zs)VL08l2VC&l@Y}jI{}4ZEWp~v2jwK)ETGpBkgm#s)PX#?lT-Dz13doY9^z+G7Bdr89*Mr+bn*)9K#NZ)$XNnu>&`
z;ua@Y8*k?1$MtJ2^D%$Cpk+2^?FTh=vF(>~uS(yo*)Vy0DiD=MJg?$Va`Gp4M~kBB
zm@ZyWKbzBVQ&;rGO^xvuCt*&G#P#(tM{IArplLRz8HH&%NN)SL!ajLsbyq9*bLG|z
z@On+!jM$n5_&RSyG?gH>>Ut`r*VWBEob(2l1N4oYydI16hDF{}IsLz;HL7ekxFRD6
zKH5@oo*$)&ik`kPV5C$If8Vif4!D&tsldMW28mPLN8IEa*w*^tHt>?&qC*gg|G3LX
zn6Gc5OdfCr+@f8cVV$Ofm1h27`nQ!o;2)!_R}E;-GXKZmuG;Gw7s*rfxm9Mx)4(L(
zr9WTQtR6=4K>+&aRkd<*7^#M;BJFat9@KEreO32?6YG)t8jKn_AX1Rec+!
zO!;R>okn;B0Pa1;WsJw@i8W8;lRdz9xs?DP0+63lLt~xoIaJOiOL343=pu^nJi-gq
z(YU_ju5hMN;sSt+!8_t}c*gqiW+xa!DqB1$I{Qb&kpZ}%PSV-NNBOPO{Y_aC+cgk7
zv1VF)R6wvAssr#Wr>0vT+(d7*H0Mn6q&y^@kPSlOr`8UugxIKUtzH(A59MN+QYeEG
zMvB+s9myB$jWn=!BQK{{)>g@&Mp4ks8;trvZbcdLH7r8{8v61U3!NBL)9YJ`=!FfL
z_i$$xwQMlt{W$BAEE6P@1w4JVUQd6&HMc||WV2dD@%T|zbSPL_*R!Z1>-3e7!gE&E
z0wFmuFV`DFu8=oWOuuQ<(TDb`58V+tR8M=k6{?!W(gBJWEfgh7r3hsH8p!-hMGRC9
zcNHKI@7Eu(6>C}TpcUAAlwZpJ1=QBsAI1fJks@MEF_QjwicWi2kP
zOJGTrt!`$AJq8wbS;qPd&?yyUgke!|dB_cDvYj@b4LM(Q1_Q1WqCcnyHoNYv=?nC{
z_3%C)Y#34Ku##s%60BJgFK7G!mLRyT1{{8IMW*i!8ke;pBX(i(wE7(t?EHknrM=;@~RK)9c4>+8|_N+|mdksym(I?t0
zZF#{=!2QQuqFeB%ve+ii(oxU{N?*0nc<
zZ>cz~b>37vsZ81`CiCWXmN{KTLRS&(oz=A@>)Nj7CF*)6duH`DNqza8-k#9glct(j
z>19vC-W9LujvISEl}ZeS=^QR!cUBcqea}3jt)A01CA3Xf54>{dr9*LT(~NfeoVG8a
z?VHu!KW{FZ%u3qo=WI<0TT?jRy!F+sGq#>vTvmSN+N8Pa!jW@FVy!dg=0&<~
z#@v#$)&9`@yg6A~^NCbaZh~A_S@*+^=Q}>lRhAV_cBi$Rq3CRPqE5Ki46;yP
zWM|p{Zqr5%+Wb?UOGAhqitWCd9k1QCdsYwG)^<~094~FWtpCU2E5%nkXPS1#8+XM`
zAiQtBsBAJ1GEjbDym0fZb_-;u&p*I?Azes1t7G@a?u{2T&gL}HJ?r(AWiZ_jcbU5E
z-0S9SM7z4%Bz?WPtlJ>HZjb{_E>53Wzd1W|vtGw_T1W=Zc2${47#XJ!MgibLT)y#q
z6g0wjYw)RTmj`xsz)PUkA(#vZ9)-*Sc@!yjN)*edys&Lo}-_2t+xC#lDHqqFA&~DVCQ3b0;=yC9{D9GrR2xlLH}{0
zMuWEy-lk7>Z{lB|Z+ABoyaNn--im{Ak>>U^WU~`+36+0DyL+DIUz+}Pk5mA|WryuPup5E1G6#)AM^|G81!n60NYvSh1*Q>7O4kmoo7g
zl(5Z$n*pxW1Tt>Ywtda}{|(8%?gXX(iLf+FnN7%6YuWEx%pn2>3hDfneI3>8V!McK
z*2!;>dJ!O%of$$VoBx(t_v;myv`K?BCDtnvU#`@cbX9D;|7N1_j95Up0o=0NF*0
zyJ;~gyn#k62+RleqReC~kh?tDY^-d?KSjZRBi!k3#n}D}rMT8)**Aqfh
zFK;nX2E6^kz}WZyazrjCeV|As&J)>4kS`GaM8D;9@Ym?SJ2ye40F37V#x;A0*D3nh
z$0G5LT3PZW`#?myh4Pa9K&0gG>FIG0yyQ|!BKq9$SxOGmZwZar{{uYo8N%oEWg*O8
zp6(Q@6E(unI
zUZjVI9sIk~*M@Csejoi!U=?&D54ONgSRZskXXk^l_Chcm&w|vQxf}iVe1hkN^doFW
z_!>Y;Is)IJG8g=%KuWQz7ijo|p*M&9U&Y2UC7P1(WA%LQD3Q=R!!J=CEY$zkqc#^pvKPysQI#J`gpM}
zdj%&aBn5shEp^q=cP3U->e3bB-%I$HPv#M{AsMLT(`Q^2#Vh|%Dp=trmmYm+4X>l`
z4d}uPo6HJ(glKWhh2W@{uty|5w|LSO7_;^`eAd2Tz!waV0yNMe>;WsH
zN6LVJ%ff(gC;_l5$$qfe`K3GPpU@lIK?^0*(saZ8gJ^jNu}=oJjjqvs
z;iLI|QaEF;fzI!_^A5rf(1X>H1T(hqau(@N9deed^u|J?DDjl^bSC@jE{){7s
zl@Vp!xaM-ftadGw2X`uNoGINjtLp^C!s4?BA_wB;b(hJkp<~`qoX+NQi~p#l-#zu_
zfgUseH(iC@E%52c1O0kQ1JKvie0P)VdKU7pTaka=ra`)q(M>$kE$SYV^!i49k3o7v
zD+iq$203tUn4~~YM@Q`P>G?-q=Xo3b!D-t8Ji?j6xg4j0O?(AXl?bZ<9w1dnvPr5&
zss_Q1P>WE9fR_YWjZ{5C11&pKuULa*A-(@hQTWb-MH*4kgs=sHRlm(hwIDrO;Uo-XZq)yvvPg`-pZ+9rMB~cii*9j<#0p0L2cY|*00000
delta 4317
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
pT3^G??88Km+
delta 457
zcmX|-PfOfD7{+HZyXvMULFlER?5ih*tkl(B7B5{D6!uS%1<^~Hbtm0mG81MJQ4#E+
zS3MZs$kadf#r
zd<`r-;7QnJQq^=c(rLzHpMQHJ9MbFZ$+ZI-MuC3O3rGzXT=?X|Le+)pVs<)aW8(&;
zSEY6}H;_{|1_WDm 200 else json_str
+ print(f" Preview: {preview}...")
+ return None
+ except Exception as e:
+ print(f" ❌ Unexpected error: {e}")
return None
async def process_investor_profile(
@@ -338,34 +396,45 @@ Return the lower and upper bounds in USD."""
if existing_company:
# Update only founded_year on existing company
company = existing_company
+ updated_fields = []
+
if company_data.get("founded_year"):
company.founded_year = company_data["founded_year"]
+ updated_fields.append(
+ f"founded_year: {company_data['founded_year']}"
+ )
+
+ # Add/update company members (key executives)
+ # First, remove existing members if updating
+ db.query(CompanyMember).filter_by(company_id=company.id).delete()
+
+ exec_count = 0
+ for exec_data in company_data.get("key_executives", []):
+ member = CompanyMember(
+ name=exec_data.get("name"),
+ role=exec_data.get("title"),
+ linkedin=exec_data.get(
+ "source_url"
+ ), # Store source URL in linkedin field
+ company_id=company.id,
+ )
+ db.add(member)
+ exec_count += 1
+
+ if exec_count > 0:
+ updated_fields.append(f"{exec_count} executives")
+
+ if updated_fields:
+ print(f" 📝 Updated: {', '.join(updated_fields)}")
+
+ return company
else:
- # Company should already be in base database, but if not found, skip
- print(
- f"⚠️ Company '{company_data['name']}' not found in base database - skipping"
- )
+ # Company not found in base database, skip
+ print(" ⚠️ Not in database - skipping")
return None
- # Add/update company members (key executives)
- # First, remove existing members if updating
- db.query(CompanyMember).filter_by(company_id=company.id).delete()
-
- for exec_data in company_data.get("key_executives", []):
- member = CompanyMember(
- name=exec_data.get("name"),
- role=exec_data.get("title"),
- linkedin=exec_data.get(
- "source_url"
- ), # Store source URL in linkedin field
- company_id=company.id,
- )
- db.add(member)
-
- return company
-
except Exception as e:
- print(f"Error saving company to database: {e}")
+ print(f" ❌ Error saving: {e}")
db.rollback()
return None
@@ -789,8 +858,11 @@ Return the lower and upper bounds in USD."""
if pd.notna(row.get("Investor"))
else None
)
+ # Try both column names for flexibility
profile_json = (
- row.get("Final Investor Profile", "")
+ row.get("Perplexity Gap Output", "")
+ if pd.notna(row.get("Perplexity Gap Output"))
+ else row.get("Final Investor Profile", "")
if pd.notna(row.get("Final Investor Profile"))
else None
)
diff --git a/app/services/report_gen.py b/app/services/report_gen.py
index 1e0c2a5..fcfe220 100644
--- a/app/services/report_gen.py
+++ b/app/services/report_gen.py
@@ -80,34 +80,70 @@ class ReportGenerator:
"thesis": 5,
}
+ # Aggregate data from all funds
+ all_sectors = set(investor_data.get("sectors", []))
+ all_stages = set()
+ all_geographies = []
+ check_ranges = []
+
+ for fund in investor_data.get("funds", []):
+ all_sectors.update(fund.get("sectors", []))
+ all_stages.update(fund.get("investment_stages", []))
+ if fund.get("geographic_focus"):
+ all_geographies.append(fund["geographic_focus"])
+ if fund.get("check_size_lower") and fund.get("check_size_upper"):
+ check_ranges.append(
+ {
+ "lower": fund["check_size_lower"],
+ "upper": fund["check_size_upper"],
+ }
+ )
+
# Sector match
- investor_sectors = set(investor_data.get("sectors", []))
project_sectors = set(project_data.get("sectors", []))
- if investor_sectors and project_sectors:
- if investor_sectors & project_sectors:
+ if all_sectors and project_sectors:
+ if all_sectors & project_sectors:
score += weights["sector"]
- # Stage match
- investor_stages = set(investor_data.get("investment_stages", []))
+ # Stage match - case insensitive comparison
project_stage = project_data.get("stage")
- if project_stage and project_stage in investor_stages:
- score += weights["stage"]
+ if project_stage and all_stages:
+ # Normalize stage names for comparison (case-insensitive)
+ normalized_stages = {
+ stage.lower().replace("_", " ") for stage in all_stages
+ }
+ project_stage_normalized = project_stage.lower().replace("_", " ")
+ if project_stage_normalized in normalized_stages:
+ score += weights["stage"]
- # Geography match
- investor_geo = (investor_data.get("geographic_focus") or "").lower()
+ # Geography match - check if any fund matches
project_geo = (project_data.get("location") or "").lower()
- if investor_geo and project_geo and investor_geo in project_geo:
+ geo_match = False
+ if all_geographies:
+ for geo in all_geographies:
+ if geo:
+ geo_lower = geo.lower()
+ # Match if investor geography is "global" or if there's a location overlap
+ if "global" in geo_lower or "worldwide" in geo_lower:
+ geo_match = True
+ break
+ if project_geo and (
+ geo_lower in project_geo or project_geo in geo_lower
+ ):
+ geo_match = True
+ break
+ if geo_match:
score += weights["geography"]
- # Check size match
+ # Check size match - check if any fund's range matches
project_valuation = project_data.get("valuation", 0)
- check_lower = investor_data.get("check_size_lower") or 0
- check_upper = investor_data.get("check_size_upper") or float("inf")
- if (
- check_lower
- and check_upper
- and check_lower <= project_valuation <= check_upper
- ):
+ check_match = False
+ if project_valuation and check_ranges:
+ for check_range in check_ranges:
+ if check_range["lower"] <= project_valuation <= check_range["upper"]:
+ check_match = True
+ break
+ if check_match:
score += weights["check_size"]
# Thesis alignment (simplified)
@@ -121,86 +157,126 @@ class ReportGenerator:
"""Generate detailed match criteria table"""
criteria = []
+ # Aggregate data from all funds
+ all_sectors = set(investor_data.get("sectors", []))
+ all_stages = set()
+ all_geographies = []
+ check_ranges = []
+
+ for fund in investor_data.get("funds", []):
+ all_sectors.update(fund.get("sectors", []))
+ all_stages.update(fund.get("investment_stages", []))
+ if fund.get("geographic_focus"):
+ all_geographies.append(fund["geographic_focus"])
+ if fund.get("check_size_lower") and fund.get("check_size_upper"):
+ check_ranges.append(
+ {
+ "lower": fund["check_size_lower"],
+ "upper": fund["check_size_upper"],
+ "fund_name": fund.get("fund_name", "Unnamed Fund"),
+ }
+ )
+
# Sector criterion
- investor_sectors = investor_data.get("sectors", [])
project_sectors = project_data.get("sectors", [])
- sector_match = (
- "Perfect" if set(investor_sectors) & set(project_sectors) else "Mismatch"
- )
+ sector_match = "Perfect" if all_sectors & set(project_sectors) else "Mismatch"
criteria.append(
{
"name": "Sector",
- "requirement": "Cybersecurity, B2B SaaS" if project_sectors else "N/A",
- "evidence": ", ".join(investor_sectors[:3])
- if investor_sectors
- else "N/A",
+ "requirement": ", ".join(project_sectors) if project_sectors else "N/A",
+ "evidence": ", ".join(list(all_sectors)[:3]) if all_sectors else "N/A",
"match": sector_match,
"weight": "30%",
}
)
- # Stage criterion
- investor_stages = investor_data.get("investment_stages", [])
+ # Stage criterion - case insensitive comparison
project_stage = project_data.get("stage", "N/A")
- stage_match = "Perfect" if project_stage in investor_stages else "Mismatch"
+ stage_match = "Mismatch"
+ if project_stage != "N/A" and all_stages:
+ # Normalize stage names for comparison
+ normalized_stages = {
+ stage.lower().replace("_", " ") for stage in all_stages
+ }
+ project_stage_normalized = project_stage.lower().replace("_", " ")
+ stage_match = (
+ "Perfect"
+ if project_stage_normalized in normalized_stages
+ else "Mismatch"
+ )
+ elif project_stage == "N/A":
+ stage_match = "N/A"
+
criteria.append(
{
"name": "Stage",
"requirement": str(project_stage),
- "evidence": ", ".join(investor_stages) if investor_stages else "N/A",
+ "evidence": ", ".join(all_stages) if all_stages else "N/A",
"match": stage_match,
"weight": "30%",
}
)
# Geography criterion
- investor_geo = investor_data.get("geographic_focus") or "N/A"
project_geo = project_data.get("location") or "N/A"
+ investor_geo_display = ", ".join(all_geographies) if all_geographies else "N/A"
+
+ # Safe comparison handling None values and "Global" matches
+ geo_match = "Mismatch"
+ if project_geo != "N/A" and all_geographies:
+ for geo in all_geographies:
+ if geo:
+ geo_lower = geo.lower()
+ # Match if investor geography is "global" or if there's a location overlap
+ if "global" in geo_lower or "worldwide" in geo_lower:
+ geo_match = "Perfect"
+ break
+ if (
+ geo_lower in project_geo.lower()
+ or project_geo.lower() in geo_lower
+ ):
+ geo_match = "Strong"
+ break
+ elif not all_geographies and project_geo == "N/A":
+ geo_match = "N/A"
- # Safe comparison handling None values
- if investor_geo == "N/A" or project_geo == "N/A":
- geo_match = (
- "N/A" if investor_geo == "N/A" and project_geo == "N/A" else "Mismatch"
- )
- else:
- investor_geo_lower = investor_geo.lower()
- project_geo_lower = project_geo.lower()
- geo_match = (
- "Strong"
- if investor_geo_lower in project_geo_lower
- or project_geo_lower in investor_geo_lower
- else "Mismatch"
- )
criteria.append(
{
"name": "Geography",
"requirement": project_geo,
- "evidence": investor_geo,
+ "evidence": investor_geo_display,
"match": geo_match,
"weight": "20%",
}
)
# Check Size criterion
- check_lower = investor_data.get("check_size_lower") or 0
- check_upper = investor_data.get("check_size_upper") or 0
project_val = project_data.get("valuation", 0)
+ # Build evidence string from all fund ranges
check_evidence = "N/A"
- if check_lower and check_upper:
- check_evidence = (
- f"€{check_lower / 1000000:.0f}M - €{check_upper / 1000000:.0f}M"
- )
- elif check_lower:
- check_evidence = f"€{check_lower / 1000000:.0f}M+"
+ if check_ranges:
+ evidence_parts = []
+ for cr in check_ranges[:3]: # Show up to 3 funds
+ range_str = (
+ f"€{cr['lower'] / 1000000:.0f}M - €{cr['upper'] / 1000000:.0f}M"
+ )
+ if cr["fund_name"]:
+ evidence_parts.append(f"{cr['fund_name']}: {range_str}")
+ else:
+ evidence_parts.append(range_str)
+ check_evidence = "; ".join(evidence_parts)
+
+ # Check if project valuation matches any fund
+ check_match = "N/A"
+ if project_val > 0 and check_ranges:
+ match_found = any(
+ cr["lower"] <= project_val <= cr["upper"] for cr in check_ranges
+ )
+ check_match = "Perfect" if match_found else "Mismatch"
+ elif project_val > 0:
+ check_match = "Strong"
- check_match = (
- "Perfect"
- if check_lower and check_upper and check_lower <= project_val <= check_upper
- else "Strong"
- if project_val > 0
- else "N/A"
- )
criteria.append(
{
"name": "Check Size",
diff --git a/app/templates/report.html b/app/templates/report.html
index 44d02ff..06c92e7 100644
--- a/app/templates/report.html
+++ b/app/templates/report.html
@@ -161,13 +161,6 @@
-
-
DACH Region:
-
- {{ investor.geographic_focus or 'N/A' }}
-
-
-
AUM (EUR million):
@@ -179,33 +172,47 @@
-
-
- Investment Stage:
-
-
- {% if investor.investment_stages %} {{
- investor.investment_stages | join(', ') }} {% else
- %} N/A {% endif %}
-
-
-
-
-
- Est. Investment Size:
-
-
- {% if investor.check_size_lower and
- investor.check_size_upper %} €{{
- '{:,.0f}'.format(investor.check_size_lower /
- 1000000) }}M - €{{
- '{:,.0f}'.format(investor.check_size_upper /
- 1000000) }}M {% elif investor.check_size_lower %}
- €{{ '{:,.0f}'.format(investor.check_size_lower /
- 1000000) }}M+ {% else %} N/A {% endif %}
+
+
Number of Funds:
+
+ {{ investor.funds | length if investor.funds else 'N/A' }}
+
+
+
+ Fund Details
+
+ {% if investor.funds %}
+ {% for fund in investor.funds %}
+
+
+ {{ fund.fund_name or 'Fund ' + loop.index|string }}
+
+
+ {% if fund.fund_size %}
+
Fund Size: €{{ '{:,.0f}'.format(fund.fund_size / 1000000) }}M
+ {% endif %}
+ {% if fund.check_size_lower and fund.check_size_upper %}
+
Check Size: €{{ '{:,.0f}'.format(fund.check_size_lower / 1000000) }}M - €{{ '{:,.0f}'.format(fund.check_size_upper / 1000000) }}M
+ {% endif %}
+ {% if fund.geographic_focus %}
+
Geography: {{ fund.geographic_focus }}
+ {% endif %}
+ {% if fund.investment_stages %}
+
Stages: {{ fund.investment_stages | join(', ') }}
+ {% endif %}
+ {% if fund.sectors %}
+
Sectors: {{ fund.sectors[:3] | join(', ') }}
+ {% endif %}
+
+
+ {% endfor %}
+ {% else %}
+
No fund information available
+ {% endif %}
+
diff --git a/investors.db b/investors.db
index c3d9648ad30c565d9786ca2624d03dad6ccaeb88..ccc9762f4ab10f7df9dc2d58ed8b80a5f8ec2317 100644
GIT binary patch
delta 2276901
zcmZ@>2Urx>+Mb#1eUaTMJG&G?5fIs3x)mg-2ndL%*l@)aH`1i2XcC>FYc!hTNmDV+
zn5xN*lX7E8@!oVzH8wzvNla1GqbdJ)7E^9^e(#gqC$h71%2(d+{oZp}*ZZw;-NF0K
zn;k>c-w}j!6YuX%*hY!{_1)2l(b)g@q@ijG9Hu{$|`kR*e7o#WgNjEw@
zgY<`$Ps=YY&x;!I=lId3x#Sp4A+KgCvnZs6sgC;RtSUW!e)@l=z`bLbNNA-c)5()s
z1wkmKF!z%zDY<)9tRh#?IyFpQ7A3d>b&LJ00;^!95uvq>F+!3^Y7Re?2&nw~6
zQN`1u+rKEyNpz~%7W0qKdQwEE(m-&&
zD$zhC!e^6?hCj#P&Xm5+
zwmqzoZqB#uq|nG|wu@U!Y^n(2C4zWKwZ=lIUovYHGc`}>`iyUD-=Z}7?+lleDEg9P
zhee!dHNvc8^2M0oYx)-0u2Pzh(ecV_uW|hO8gHst=r9tTP?ayV;txxm6*d(~X$H`S
zIoCcN`PZjYgtc}f5ic;Mf`*!ql9FFnSHBD;-(sWDJ*9RBn$XG9$o&
zoo5>vT!z1<=E;)CQrk+c^wb<%7AYN@XZwlVy3iIuQ4LbmLR%uGRCf@=P;{>-gy)1#uD0DlY0jYK
zYhdX3YyY-fZZRvxNW=;GMM5bx4nDm02HRI^&A`&;<_1(cH{XUd<08XQwrJD;Z7^BV
z?JI1fl?r164CbKvO|)%|?LBZ*a!2%@5>mvrdY^=Dv|UfdD25YeBArkgEiTiqCYy1N
zVXpRm<yxPH8WNGUBAEL0H;DYeapC`})F
zqs^A`k7e3%`$uu?j{fqsLsQ99t
zL7RJQy1z{)RhqlO_K=cM=?DY)HKFNO4=AUodX2A|K2hk%uO)V~?L*QgvSr{G`K4kw
zm4_{**;mz!23~=EJ9UrkNlJ4b-F&Yt`JV&K+@-tCMp1@mP{~2V9Ye(N#YQ4Y(5w+j
zYFJ9jl(IsU+>=V9Wd}Vh%D=<*mQK@$hr6_?28A(TeE(Q^k2J8=_P$bK{+@g$?2w7%
zMOO*zb-53|FC!&omcOQEQlM#R)C_;sQgpN$On4p_qerPbY!6VHPtdU)wh{jrp(joH
zWSh-O9vC$|k~EOFlS)b1Yonr~nhE6*i%oIB?4({YWg6oRO?rvks(V8Ff#$IKd!iXx
z58gJ75hLrspx4w2bMgC&DsqcUrc}%*M$wyWcJ%tUL(QnV?Z6(UzIx5W{tA-bOyR~A
z5G1Pu!kD1(%>LQ-yhU>+XgrPnI$v{3qaPi5khcHRd{U%Kmuz#iQsPzHhq1!}Cs>IN
z!jf)|rFu+pM2E4!kWW6VpRC)a6*Z&P8&ti@-xMfk;pPRLm@FuaM5It&Eac+xPVqG~
zw9c%D_jo4S!cjw~U5ok-*;FX%vdxIx&&LiymG!X}q#J3!jnbS&_l~p=28zFhv`nE$
zfS}A3VfTUWoa_tus?pwRyA5TpwVBZPc>5wsa~k!=UwfASGaPy{#Tmh89vd9P-$v{avjvFY-YriP