# 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"""
Payback
{payback_years}
Time to break even
""" k2 = f"""
ROI (5y)
{roi_str}
Risk-adjusted
""" k3 = f"""
IRR (annual)
{irr_str}
From monthly cashflows
""" k4 = f"""
NPV
{npv_str}
Discounted at your rate
""" return k1, k2, k3, k4 # ========================= # APP LAYOUT # ========================= def create_app(): with gr.Blocks(css=CSS, theme=THEME) as demo: # HEADER gr.HTML("""
VisaTier — Immigration ROI Simulator
Built for founders • MVP
""") # Methodology + Notes (always visible) gr.HTML("""
Methodology
How we calculate (simplified):
• Baseline owner cashflow = after corporate & personal taxes – living – ongoing.
• Scenario = revenue × multiplier, margin delta, destination taxes & living.
• Monthly uplift ΔCF → cumulative cashflow → Payback, ROI, IRR, NPV.
• Risk‑adjusted (success probability), discounted at your chosen rate.
""") gr.HTML("""
Notes: Illustrative model, not tax or legal advice. Results depend on execution and personal circumstances.
""") # HERO gr.HTML("""

Model the financial impact of relocation

Simulate revenue lift, taxes and living costs — get a risk‑adjusted payback date in minutes.

MVP Founders Free to try
""") # ===== MAIN SIMULATOR ===== with gr.Row(): with gr.Column(scale=5): dest = gr.Dropdown(COUNTRIES, value="UAE (Dubai)", label="Destination template") with gr.Row(): rev0 = gr.Number(value=30000, label="Baseline revenue (€/month)") margin0 = gr.Slider(value=25, minimum=1, maximum=70, step=1, label="Baseline EBITDA margin (%)") with gr.Row(): corp0 = gr.Slider(value=20, minimum=0, maximum=50, step=1, label="Baseline corporate tax (%)") pers0 = gr.Slider(value=10, minimum=0, maximum=50, step=1, label="Baseline personal tax (%)") with gr.Row(): living0 = gr.Number(value=4000, label="Baseline living cost (€/month)") ongoing0 = gr.Number(value=0, label="Other ongoing (€/month)") gr.Markdown("**Destination adjustments**") with gr.Row(): rev_mult = gr.Slider(value=3.0, minimum=0.5, maximum=5.0, step=0.1, label="Revenue multiplier (×)") margin_delta = gr.Slider(value=5.0, minimum=-20, maximum=30, step=0.5, label="Margin delta (pp)") with gr.Row(): corp1 = gr.Slider(value=9, minimum=0, maximum=50, step=1, label="Destination corporate tax (%)") pers1 = gr.Slider(value=0, minimum=0, maximum=50, step=1, label="Destination personal tax (%)") with gr.Row(): living1 = gr.Number(value=9000, label="Destination living cost (€/month)") ongoing1 = gr.Number(value=1500, label="Other ongoing (€/month)") with gr.Accordion("Investments & risk", open=False): with gr.Row(): capex_once = gr.Number(value=35000, label="One-time setup (€)") horizon_m = gr.Slider(value=36, minimum=6, maximum=60, step=1, label="Horizon (months)") with gr.Row(): discount_a = gr.Slider(value=12, minimum=0, maximum=40, step=1, label="Annual discount rate (%)") success = gr.Slider(value=75, minimum=10, maximum=100, step=1, label="Success probability (%)") estimate_btn = gr.Button("Estimate", variant="primary") with gr.Column(scale=7): gr.Markdown("**Results (teaser)** — first 2 KPIs.") kpi1 = gr.HTML("
Payback
Time to break even
") kpi2 = gr.HTML("
ROI (5y)
Risk-adjusted
") kpi3 = gr.HTML("
IRR (annual)
From monthly cashflows
", visible=False) kpi4 = gr.HTML("
NPV
Discounted at your rate
", visible=False) chart = gr.Plot(visible=False) cta_btn = gr.HTML(f'Book diagnostic', visible=False) state_result = gr.State(value=None) state_unlocked = gr.State(value=False) def on_estimate( dest, rev0, margin0, corp0, pers0, living0, ongoing0, rev_mult, margin_delta, corp1, pers1, living1, ongoing1, capex_once, horizon_m, discount_a, success ): # Apply template defaults if user left UAE defaults if dest in COUNTRY_CONFIG: cfg = COUNTRY_CONFIG[dest] # if user did not change from UAE defaults, swap to chosen if dest != "UAE (Dubai)": if rev_mult == 3.0: rev_mult = cfg["rev_mult"] if margin_delta == 5.0: margin_delta = cfg["margin_delta_pp"] if corp1 == 9: corp1 = cfg["corp_tax"] * 100.0 if pers1 == 0: pers1 = cfg["pers_tax"] * 100.0 if living1 == 9000: living1 = cfg["living_month"] if ongoing1 == 1500: ongoing1 = cfg["ongoing_month"] if capex_once == 35000: capex_once = cfg["setup_once"] result = compute_monthly_delta_cashflow( rev0, margin0, corp0, pers0, living0, ongoing0, dest, rev_mult, margin_delta, corp1, pers1, living1, ongoing1, capex_once, int(horizon_m), discount_a, success ) k1, k2, k3, k4 = render_kpis(result) return ( k1, k2, k3, k4, build_cashflow_chart(result["months"], result["cum_values"], result["payback_months"]), result, True ) estimate_btn.click( on_estimate, inputs=[dest, rev0, margin0, corp0, pers0, living0, ongoing0, rev_mult, margin_delta, corp1, pers1, living1, ongoing1, capex_once, horizon_m, discount_a, success], outputs=[kpi1, kpi2, kpi3, kpi4, chart, state_result, cta_btn] ) # ===== COMPARISON BLOCK ===== gr.Markdown("## Compare destinations — Dubai / UK / USA / Spain / Ireland") with gr.Row(): with gr.Column(scale=5): gr.Markdown("**Assumptions shared across comparison** (you can tweak):") rev0_c = gr.Number(value=30000, label="Baseline revenue (€/month)") margin0_c = gr.Slider(value=25, minimum=1, maximum=70, step=1, label="Baseline EBITDA margin (%)") corp0_c = gr.Slider(value=20, minimum=0, maximum=50, step=1, label="Baseline corporate tax (%)") pers0_c = gr.Slider(value=10, minimum=0, maximum=50, step=1, label="Baseline personal tax (%)") living0_c = gr.Number(value=4000, label="Baseline living cost (€/month)") ongoing0_c = gr.Number(value=0, label="Other ongoing (€/month)") with gr.Row(): capex_c = gr.Number(value=30000, label="One-time setup (€)") horizon_c = gr.Slider(value=36, minimum=6, maximum=60, step=1, label="Horizon (months)") with gr.Row(): discount_c = gr.Slider(value=12, minimum=0, maximum=40, step=1, label="Annual discount rate (%)") success_c = gr.Slider(value=75, minimum=10, maximum=100, step=1, label="Success probability (%)") compare_btn = gr.Button("Run comparison", variant="primary") with gr.Column(scale=7): comp_kpis = gr.HTML("
DestinationPayback (yrs)ROI (5y)IRR (annual)NPV (€)
") comp_bar_roi = gr.Plot() comp_bar_payback = gr.Plot() def run_comparison(rev0, margin0, corp0, pers0, living0, ongoing0, capex, horizon, discount, success): rows = [] roi_vals = [] payback_vals = [] for name in COUNTRIES: cfg = COUNTRY_CONFIG[name] res = compute_monthly_delta_cashflow( rev0, margin0, corp0, pers0, living0, ongoing0, name, cfg["rev_mult"], cfg["margin_delta_pp"], cfg["corp_tax"]*100.0, cfg["pers_tax"]*100.0, cfg["living_month"], cfg["ongoing_month"], cfg["setup_once"] if capex == 30000 else capex, # если не трогали — можно оставить шаблонный setup int(horizon), discount, success ) pb = "Never" if res["payback_years"] == float("inf") else f"{res['payback_years']:.1f}" rows.append(f"{name}{pb}{res['total_5yr_roi']:.1f}%{res['irr_annual_pct']:.1f}%€{res['npv']:,.0f}") roi_vals.append(res["total_5yr_roi"]) payback_vals.append(res["payback_years"] if res["payback_years"] != float("inf") else None) table_html = "
" + "".join(rows) + "
DestinationPayback (yrs)ROI (5y)IRR (annual)NPV (€)
" bar_roi = build_comparison_bar(COUNTRIES, roi_vals, "ROI (5y) by destination", "ROI %") # For payback: set large number for None to visualize; but better leave None & Plotly will skip pb_plot_vals = [v if v is not None else None for v in payback_vals] bar_payback = build_comparison_bar(COUNTRIES, pb_plot_vals, "Payback (years) by destination", "Years") return table_html, bar_roi, bar_payback compare_btn.click( run_comparison, inputs=[rev0_c, margin0_c, corp0_c, pers0_c, living0_c, ongoing0_c, capex_c, horizon_c, discount_c, success_c], outputs=[comp_kpis, comp_bar_roi, comp_bar_payback] ) # FOOTER gr.HTML("""""".format(BOOK_LINK)) return demo # HF expects a global `demo` demo = create_app() if __name__ == "__main__": demo.launch()