Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Deploy

on:
push:
branches: [main]

jobs:
deploy-api:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write

steps:
- uses: actions/checkout@v4

- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}

- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2

- name: Configure Docker for Artifact Registry
run: gcloud auth configure-docker europe-west1-docker.pkg.dev

- name: Build and push Docker image
run: |
docker build -t europe-west1-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/givecalc/api:${{ github.sha }} .
docker build -t europe-west1-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/givecalc/api:latest .
docker push europe-west1-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/givecalc/api:${{ github.sha }}
docker push europe-west1-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/givecalc/api:latest

- name: Deploy to Cloud Run
run: |
gcloud run deploy givecalc \
--image europe-west1-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/givecalc/api:${{ github.sha }} \
--region europe-west1 \
--platform managed \
--allow-unauthenticated \
--memory 2Gi \
--cpu 2 \
--timeout 300 \
--min-instances 0 \
--max-instances 10
15 changes: 15 additions & 0 deletions api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
StatesResponse,
TargetDonationRequest,
TargetDonationResponse,
TaxBreakdown,
TaxProgram,
TaxProgramsResponse,
)
Expand Down Expand Up @@ -222,6 +223,8 @@ async def calculate_donation(request: CalculateRequest):
baseline_metrics = calculate_donation_metrics(situation, 0)
baseline_net_tax = float(baseline_metrics["baseline_income_tax"][0])
baseline_net_income = float(baseline_metrics["baseline_net_income"][0])
baseline_federal_tax = float(baseline_metrics["federal_income_tax"][0])
baseline_state_tax = float(baseline_metrics["state_income_tax"][0])

# Calculate metrics at specified donation
donation_metrics = calculate_donation_metrics(
Expand All @@ -231,6 +234,8 @@ async def calculate_donation(request: CalculateRequest):
net_income_at_donation = float(
donation_metrics["baseline_net_income"][0]
)
donation_federal_tax = float(donation_metrics["federal_income_tax"][0])
donation_state_tax = float(donation_metrics["state_income_tax"][0])

# Calculate full donation curve
df = calculate_donation_effects(situation)
Expand Down Expand Up @@ -276,6 +281,16 @@ async def calculate_donation(request: CalculateRequest):
net_tax_at_donation=net_tax_at_donation,
tax_savings=baseline_net_tax - net_tax_at_donation,
marginal_savings_rate=marginal_savings_rate,
baseline_tax_breakdown=TaxBreakdown(
federal=baseline_federal_tax,
state=baseline_state_tax,
total=baseline_federal_tax + baseline_state_tax,
),
donation_tax_breakdown=TaxBreakdown(
federal=donation_federal_tax,
state=donation_state_tax,
total=donation_federal_tax + donation_state_tax,
),
baseline_net_income=baseline_net_income,
net_income_after_donation=net_income_at_donation
- request.donation_amount,
Expand Down
12 changes: 12 additions & 0 deletions api/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ class DonationDataPoint(BaseModel):
net_income: float


class TaxBreakdown(BaseModel):
"""Federal and state tax breakdown."""

federal: float
state: float
total: float


class CalculateResponse(BaseModel):
"""Response from donation calculation."""

Expand All @@ -96,6 +104,10 @@ class CalculateResponse(BaseModel):
tax_savings: float
marginal_savings_rate: float

# Tax breakdown (federal vs state)
baseline_tax_breakdown: TaxBreakdown
donation_tax_breakdown: TaxBreakdown

# Net income metrics
baseline_net_income: float
net_income_after_donation: float
Expand Down
Binary file added docs/images/givecalc-screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
54 changes: 2 additions & 52 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import InputForm from "./components/InputForm";
import Results from "./components/Results";
import TaxInfo from "./components/TaxInfo";
import {
useStates,
useCalculateDonation,
useCalculateTargetDonation,
} from "./hooks/useCalculation";
import { STATES } from "./lib/states";
import type {
FormState,
CalculateResponse,
Expand Down Expand Up @@ -67,23 +67,6 @@ function Calculator() {
useState<TargetDonationResponse | null>(null);
const cacheRef = useRef<ResultCache>(new Map());

const {
data: statesData,
isLoading: statesLoading,
isError: statesError,
error: statesErrorData,
} = useStates();

// Debug logging
console.log(
"States loading:",
statesLoading,
"Data:",
statesData,
"Error:",
statesError,
statesErrorData,
);
const calculateMutation = useCalculateDonation();
const targetMutation = useCalculateTargetDonation();

Expand Down Expand Up @@ -156,39 +139,6 @@ function Calculator() {
const hasError = calculateMutation.isError || targetMutation.isError;
const error = calculateMutation.error || targetMutation.error;

if (statesLoading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-500" />
</div>
);
}

if (statesError) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="bg-red-50 border border-red-200 rounded-lg p-6 max-w-md">
<h2 className="text-lg font-semibold text-red-700 mb-2">
Failed to load states
</h2>
<p className="text-red-600 text-sm">
{statesErrorData instanceof Error
? statesErrorData.message
: "Could not connect to API"}
</p>
<button
onClick={() => window.location.reload()}
className="mt-4 px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700"
>
Retry
</button>
</div>
</div>
);
}

const states = statesData?.states || [];

return (
<div className="min-h-screen bg-gray-50">
<Header />
Expand All @@ -200,7 +150,7 @@ function Calculator() {
<InputForm
formState={formState}
setFormState={setFormState}
states={states}
states={STATES}
/>
<TaxInfo stateCode={formState.state_code} />
{/* Sticky Calculate Button - attached to left panel */}
Expand Down
103 changes: 72 additions & 31 deletions frontend/src/components/Results.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,41 +142,82 @@ export default function Results({
</div>
</div>

{/* Detailed Breakdown */}
{/* Tax Breakdown Table */}
{result && !isTarget && (
<div className="bg-white rounded-lg shadow-md p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Detailed breakdown
Tax breakdown
</h3>
<div className="grid grid-cols-2 gap-4 text-sm">
<div className="flex justify-between py-2 border-b border-gray-100">
<span className="text-gray-600">
Baseline net tax (no donation)
</span>
<span className="font-medium">
{formatCurrency(result.baseline_net_tax)}
</span>
</div>
<div className="flex justify-between py-2 border-b border-gray-100">
<span className="text-gray-600">
Net tax at {formatCurrency(result.donation_amount)}
</span>
<span className="font-medium">
{formatCurrency(result.net_tax_at_donation)}
</span>
</div>
<div className="flex justify-between py-2 border-b border-gray-100">
<span className="text-gray-600">Baseline net income</span>
<span className="font-medium">
{formatCurrency(result.baseline_net_income)}
</span>
</div>
<div className="flex justify-between py-2 border-b border-gray-100">
<span className="text-gray-600">Net income after donation</span>
<span className="font-medium">
{formatCurrency(result.net_income_after_donation)}
</span>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50">
<th className="text-left py-3 px-4 font-semibold text-gray-700"></th>
<th className="text-right py-3 px-4 font-semibold text-gray-700">
Without donation
</th>
<th className="text-right py-3 px-4 font-semibold text-gray-700">
With {formatCurrency(result.donation_amount)}
</th>
<th className="text-right py-3 px-4 font-semibold text-primary-700">
Savings
</th>
</tr>
</thead>
<tbody>
<tr className="border-b border-gray-100">
<td className="py-3 px-4 font-medium text-gray-900">
Federal income tax
</td>
<td className="text-right py-3 px-4 text-gray-600">
{formatCurrency(result.baseline_tax_breakdown.federal)}
</td>
<td className="text-right py-3 px-4 text-gray-600">
{formatCurrency(result.donation_tax_breakdown.federal)}
</td>
<td className="text-right py-3 px-4 font-medium text-primary-600">
{formatCurrency(
result.baseline_tax_breakdown.federal -
result.donation_tax_breakdown.federal,
)}
</td>
</tr>
<tr className="border-b border-gray-100">
<td className="py-3 px-4 font-medium text-gray-900">
State income tax
</td>
<td className="text-right py-3 px-4 text-gray-600">
{formatCurrency(result.baseline_tax_breakdown.state)}
</td>
<td className="text-right py-3 px-4 text-gray-600">
{formatCurrency(result.donation_tax_breakdown.state)}
</td>
<td className="text-right py-3 px-4 font-medium text-primary-600">
{formatCurrency(
result.baseline_tax_breakdown.state -
result.donation_tax_breakdown.state,
)}
</td>
</tr>
<tr className="bg-primary-50">
<td className="py-3 px-4 font-semibold text-gray-900">
Total
</td>
<td className="text-right py-3 px-4 font-semibold text-gray-900">
{formatCurrency(result.baseline_tax_breakdown.total)}
</td>
<td className="text-right py-3 px-4 font-semibold text-gray-900">
{formatCurrency(result.donation_tax_breakdown.total)}
</td>
<td className="text-right py-3 px-4 font-bold text-primary-600">
{formatCurrency(
result.baseline_tax_breakdown.total -
result.donation_tax_breakdown.total,
)}
</td>
</tr>
</tbody>
</table>
</div>
</div>
)}
Expand Down
60 changes: 60 additions & 0 deletions frontend/src/lib/states.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Hardcoded US states list to avoid blocking UI on API cold start.
* States don't change, so this is safe to cache in the frontend.
*/

import type { StateInfo } from "./types";

export const STATES: StateInfo[] = [
{ code: "AL", name: "Alabama", has_special_programs: false },
{ code: "AK", name: "Alaska", has_special_programs: false },
{ code: "AZ", name: "Arizona", has_special_programs: true },
{ code: "AR", name: "Arkansas", has_special_programs: false },
{ code: "CA", name: "California", has_special_programs: false },
{ code: "CO", name: "Colorado", has_special_programs: true },
{ code: "CT", name: "Connecticut", has_special_programs: false },
{ code: "DE", name: "Delaware", has_special_programs: false },
{ code: "FL", name: "Florida", has_special_programs: false },
{ code: "GA", name: "Georgia", has_special_programs: false },
{ code: "HI", name: "Hawaii", has_special_programs: false },
{ code: "ID", name: "Idaho", has_special_programs: false },
{ code: "IL", name: "Illinois", has_special_programs: false },
{ code: "IN", name: "Indiana", has_special_programs: false },
{ code: "IA", name: "Iowa", has_special_programs: false },
{ code: "KS", name: "Kansas", has_special_programs: false },
{ code: "KY", name: "Kentucky", has_special_programs: false },
{ code: "LA", name: "Louisiana", has_special_programs: false },
{ code: "ME", name: "Maine", has_special_programs: false },
{ code: "MD", name: "Maryland", has_special_programs: false },
{ code: "MA", name: "Massachusetts", has_special_programs: false },
{ code: "MI", name: "Michigan", has_special_programs: false },
{ code: "MN", name: "Minnesota", has_special_programs: false },
{ code: "MS", name: "Mississippi", has_special_programs: true },
{ code: "MO", name: "Missouri", has_special_programs: false },
{ code: "MT", name: "Montana", has_special_programs: false },
{ code: "NE", name: "Nebraska", has_special_programs: false },
{ code: "NV", name: "Nevada", has_special_programs: false },
{ code: "NH", name: "New Hampshire", has_special_programs: true },
{ code: "NJ", name: "New Jersey", has_special_programs: false },
{ code: "NM", name: "New Mexico", has_special_programs: false },
{ code: "NY", name: "New York", has_special_programs: false },
{ code: "NC", name: "North Carolina", has_special_programs: false },
{ code: "ND", name: "North Dakota", has_special_programs: false },
{ code: "OH", name: "Ohio", has_special_programs: false },
{ code: "OK", name: "Oklahoma", has_special_programs: false },
{ code: "OR", name: "Oregon", has_special_programs: false },
{ code: "PA", name: "Pennsylvania", has_special_programs: false },
{ code: "RI", name: "Rhode Island", has_special_programs: false },
{ code: "SC", name: "South Carolina", has_special_programs: false },
{ code: "SD", name: "South Dakota", has_special_programs: false },
{ code: "TN", name: "Tennessee", has_special_programs: false },
{ code: "TX", name: "Texas", has_special_programs: false },
{ code: "UT", name: "Utah", has_special_programs: false },
{ code: "VT", name: "Vermont", has_special_programs: true },
{ code: "VA", name: "Virginia", has_special_programs: false },
{ code: "WA", name: "Washington", has_special_programs: false },
{ code: "WV", name: "West Virginia", has_special_programs: false },
{ code: "WI", name: "Wisconsin", has_special_programs: false },
{ code: "WY", name: "Wyoming", has_special_programs: false },
{ code: "DC", name: "District of Columbia", has_special_programs: false },
];
8 changes: 8 additions & 0 deletions frontend/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,20 @@ export interface DonationDataPoint {
net_income: number;
}

export interface TaxBreakdown {
federal: number;
state: number;
total: number;
}

export interface CalculateResponse {
donation_amount: number;
baseline_net_tax: number;
net_tax_at_donation: number;
tax_savings: number;
marginal_savings_rate: number;
baseline_tax_breakdown: TaxBreakdown;
donation_tax_breakdown: TaxBreakdown;
baseline_net_income: number;
net_income_after_donation: number;
curve: DonationDataPoint[];
Expand Down
Loading