# app.py # VisaTier — Immigration ROI Simulator (Gradio) # • Header + Methodology + Notes (always visible) # • Main simulator (inputs, KPI tiles, cashflow chart, CTA) # • Country comparison block (Dubai / UK / USA / Spain / Ireland) # • Footer # # Assumptions are illustrative; users can tweak via inputs. import math import numpy as np import gradio as gr import plotly.graph_objects as go # ========================= # LOOK & FEEL # ========================= CSS = """ :root { --vt-primary:#2563EB; --vt-accent:#10B981; --vt-danger:#EF4444; --vt-ink:#0F172A; --vt-muted:#64748B; --radius:16px; } .gradio-container { max-width: 1160px !important; margin: 0 auto; } /* HEADER + FOOTER */ .vt-header { background:#0F172A; color:#E2E8F0; padding:14px 18px; border-radius:12px; display:flex; align-items:center; justify-content:space-between; margin-bottom:14px; } .vt-header .title { font-weight:800; font-size:18px; letter-spacing:0.2px; } .vt-header .right { font-size:12px; color:#CBD5E1; } .vt-footer { margin-top:24px; color:#94A3B8; font-size:12px; text-align:center; padding:12px; border-top:1px solid #E2E8F0; } /* HERO / BADGES */ .vt-hero { border-radius: var(--radius); padding: 18px 20px; background: linear-gradient(135deg, rgba(37,99,235,.10), rgba(16,185,129,.10)); border: 1px solid #E2E8F0; margin-bottom:16px; } .vt-badge { display:inline-block; font-size:12px; padding:4px 8px; border-radius:9999px; background:#E2E8F0; color:#334155; margin-right:6px; } /* KPI */ .vt-kpi { border-radius: var(--radius); padding:16px; background:#FFFFFF; border:1px solid #E2E8F0; margin-bottom:10px; } .vt-kpi .label { font-size:12px; color:#64748B; } .vt-kpi .value { font-size:28px; font-weight:800; color:#0F172A; line-height:1.1; } .vt-kpi.good .value { color: var(--vt-accent); } .vt-kpi.warn .value { color: var(--vt-danger); } .vt-note { font-size:12px; color:#64748B; } /* METHOD block */ .vt-method { border-radius:12px; border:1px solid #E2E8F0; background:#FFFFFF; padding:12px 14px; margin-bottom:10px; } /* CTA button as link */ .vt-cta { display:inline-block; background:#2563EB; color:#FFFFFF!important; padding:10px 14px; border-radius:10px; font-weight:700; text-decoration:none; margin-top:10px; } .vt-cta:hover { background:#1D4ED8; } .table-like { border:1px solid #E2E8F0; border-radius:12px; overflow:hidden; } .table-like table { width:100%; border-collapse:collapse; font-size:14px; } .table-like th, .table-like td { padding:10px 12px; border-bottom:1px solid #E2E8F0; text-align:left; } .table-like th { background:#F8FAFC; color:#0F172A; font-weight:700; } .table-like tr:last-child td { border-bottom: none; } """ THEME = gr.themes.Soft( primary_hue="blue", neutral_hue="slate" ).set( body_background_fill="#F8FAFC", body_text_color="#0F172A", button_primary_background_fill="#2563EB", button_primary_background_fill_hover="#1D4ED8", ) BOOK_LINK = "https://calendly.com/your-team/diagnostic" # <-- поставь свою ссылку # ========================= # COUNTRY TEMPLATES (assumptions) # ========================= COUNTRIES = ["UAE (Dubai)", "UK", "USA (FL/TX/NV)", "Spain", "Ireland"] COUNTRY_CONFIG = { "UAE (Dubai)": {"corp_tax": 0.09, "pers_tax": 0.00, "rev_mult": 3.0, "margin_delta_pp": 5.0, "living_month": 9000.0, "ongoing_month": 1500.0, "setup_once": 35000.0}, "UK": {"corp_tax": 0.25, "pers_tax": 0.27, "rev_mult": 1.5, "margin_delta_pp": 2.0, "living_month": 6200.0, "ongoing_month": 1100.0, "setup_once": 18000.0}, "USA (FL/TX/NV)":{"corp_tax": 0.21, "pers_tax": 0.22, "rev_mult": 1.8, "margin_delta_pp": 4.0, "living_month": 7000.0, "ongoing_month": 1300.0, "setup_once": 20000.0}, "Spain": {"corp_tax": 0.25, "pers_tax": 0.24, "rev_mult": 1.4, "margin_delta_pp": 2.0, "living_month": 5000.0, "ongoing_month": 1000.0, "setup_once": 20000.0}, "Ireland": {"corp_tax": 0.125, "pers_tax": 0.22, "rev_mult": 1.6, "margin_delta_pp": 3.0, "living_month": 6500.0, "ongoing_month": 1200.0, "setup_once": 25000.0}, } # ========================= # FINANCE HELPERS # ========================= def clamp(x, lo, hi): return max(lo, min(hi, x)) def monthly_rate(annual_rate): return (1.0 + annual_rate) ** (1.0 / 12.0) - 1.0 def irr_bisection(cash, lo=-0.99, hi=5.0, iters=100, tol=1e-7): def npv(rate): return sum(cf / ((1 + rate) ** t) for t, cf in enumerate(cash)) f_lo, f_hi = npv(lo), npv(hi) if f_lo * f_hi > 0: return None for _ in range(iters): mid = (lo + hi) / 2 v = npv(mid) if abs(v) < tol: return mid if v > 0: lo = mid else: hi = mid return (lo + hi) / 2 def ramp_factor(m): return 0.6 if m <= 6 else (0.8 if m <= 12 else 1.0) # ========================= # CORE MODEL (monthly) # ========================= def compute_monthly_delta_cashflow( rev0, margin0_pct, corp0_pct, pers0_pct, living0, ongoing0, dest, rev_mult, margin_delta_pp, corp1_pct, pers1_pct, living1, ongoing1, capex_once, horizon_m, discount_annual_pct, success_pct ): m0 = clamp(margin0_pct / 100.0, 0.0, 0.9) ct0 = clamp(corp0_pct / 100.0, 0.0, 0.6) pt0 = clamp(pers0_pct / 100.0, 0.0, 0.6) mult = max(0.0, rev_mult) mdelta = margin_delta_pp / 100.0 ct1 = clamp(corp1_pct / 100.0, 0.0, 0.6) pt1 = clamp(pers1_pct / 100.0, 0.0, 0.6) p = clamp(success_pct / 100.0, 0.01, 1.0) mr = monthly_rate(discount_annual_pct / 100.0) base_profit0 = rev0 * m0 after_tax0 = base_profit0 * (1 - ct0) * (1 - pt0) - living0 - ongoing0 rev1 = rev0 * mult m1 = clamp(m0 + mdelta, 0.01, 0.9) base_profit1 = rev1 * m1 after_tax1 = base_profit1 * (1 - ct1) * (1 - pt1) - living1 - ongoing1 delta_monthly = after_tax1 - after_tax0 cash = [-capex_once] cum = -capex_once months = [0] cum_series = [cum] payback_m = math.inf for m in range(1, horizon_m + 1): cf = delta_monthly * ramp_factor(m) * p cash.append(cf) cum += cf months.append(m) cum_series.append(cum) if math.isinf(payback_m) and cum >= 0: payback_m = m npv = sum(cf / ((1 + mr) ** t) for t, cf in enumerate(cash)) roi5y = (npv / capex_once * 100.0) if capex_once > 0 else (0.0 if npv <= 0 else math.inf) irr_m = irr_bisection(cash) irr_annual = ((1 + irr_m) ** 12 - 1) * 100.0 if irr_m is not None else 0.0 return { "npv": npv, "total_5yr_roi": roi5y, "payback_months": payback_m, "payback_years": (payback_m / 12.0) if not math.isinf(payback_m) else float("inf"), "irr_annual_pct": irr_annual, "months": months, "cum_values": cum_series, "delta_monthly": delta_monthly, } # ========================= # PLOTS # ========================= def build_cashflow_chart(months, cum_values, payback_month=None): fig = go.Figure() fig.add_trace(go.Scatter( x=months, y=cum_values, mode="lines", line=dict(width=3, color="#2563EB"), fill='tozeroy', fillcolor="rgba(37,99,235,0.08)", name="Cumulative ΔCF" )) fig.add_hline(y=0, line_width=1, line_dash="dot", line_color="#94A3B8") if payback_month not in (None, float("inf")): fig.add_vline( x=payback_month, line_width=1, line_dash="dot", line_color="#2563EB", annotation_text="Break-even", annotation_position="top right" ) fig.update_layout( margin=dict(l=20, r=20, t=30, b=20), xaxis_title="Month", yaxis_title="Cumulative €", plot_bgcolor="#FFFFFF", paper_bgcolor="#FFFFFF", showlegend=False, height=380 ) return fig def build_comparison_bar(names, values, title, ytitle): fig = go.Figure() fig.add_bar(x=names, y=values, marker_color="#2563EB") fig.update_layout( title=title, xaxis_title="Destination", yaxis_title=ytitle, margin=dict(l=20, r=20, t=50, b=40), plot_bgcolor="#FFFFFF", paper_bgcolor="#FFFFFF", height=380 ) return fig # ========================= # UI HELPERS # ========================= def render_kpis(result): payback_years = "Never" if result["payback_years"] == float("inf") else f'{result["payback_years"]:.1f} years' roi_str = f'{result["total_5yr_roi"]:.1f}%' irr_str = f'{result["irr_annual_pct"]:.1f}%' npv_str = f'€{result["npv"]:,.0f}' k1 = f"""
Simulate revenue lift, taxes and living costs — get a risk‑adjusted payback date in minutes.
MVP Founders Free to tryDestination | Payback (yrs) | ROI (5y) | IRR (annual) | NPV (€) |
---|
Destination | Payback (yrs) | ROI (5y) | IRR (annual) | NPV (€) |
---|