taxforge 0.9.20__py3-none-any.whl → 0.9.22__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aitax/__init__.py +1 -1
- aitax/aitax.py +286 -83
- aitax/cli.py +1 -1
- aitax/components.py +78 -0
- aitax/document_extractor.py +51 -14
- aitax/persistence.py +197 -0
- aitax/state.py +41 -6
- {taxforge-0.9.20.dist-info → taxforge-0.9.22.dist-info}/METADATA +1 -1
- taxforge-0.9.22.dist-info/RECORD +12 -0
- taxforge-0.9.20.dist-info/RECORD +0 -11
- {taxforge-0.9.20.dist-info → taxforge-0.9.22.dist-info}/WHEEL +0 -0
- {taxforge-0.9.20.dist-info → taxforge-0.9.22.dist-info}/entry_points.txt +0 -0
- {taxforge-0.9.20.dist-info → taxforge-0.9.22.dist-info}/top_level.txt +0 -0
aitax/__init__.py
CHANGED
aitax/aitax.py
CHANGED
|
@@ -6,10 +6,32 @@ from .state import TaxAppState
|
|
|
6
6
|
from .components import (
|
|
7
7
|
COLORS, fintech_card, mercury_card, gradient_button, outline_button, stat_card,
|
|
8
8
|
section_header, empty_state, badge, data_row, document_card,
|
|
9
|
-
nav_bar, page_container, api_key_modal,
|
|
9
|
+
nav_bar, page_container, api_key_modal, processing_overlay,
|
|
10
10
|
)
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
# ============== Styled Input Helper ==============
|
|
14
|
+
def styled_input(placeholder: str, value, on_change, input_type: str = "text", width: str = "100%") -> rx.Component:
|
|
15
|
+
"""Styled input with fintech theme."""
|
|
16
|
+
return rx.input(
|
|
17
|
+
placeholder=placeholder,
|
|
18
|
+
value=value,
|
|
19
|
+
on_change=on_change,
|
|
20
|
+
type=input_type,
|
|
21
|
+
width=width,
|
|
22
|
+
background="white",
|
|
23
|
+
border=f"1px solid {COLORS['border']}",
|
|
24
|
+
color=COLORS["text_primary"],
|
|
25
|
+
_placeholder={"color": COLORS["text_muted"]},
|
|
26
|
+
padding="10px 12px",
|
|
27
|
+
border_radius="8px",
|
|
28
|
+
_focus={
|
|
29
|
+
"border_color": COLORS["accent_primary"],
|
|
30
|
+
"box_shadow": f"0 0 0 3px {COLORS['accent_light']}"
|
|
31
|
+
},
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
13
35
|
# ============== Upload Zone (Compact) ==============
|
|
14
36
|
def upload_zone_compact() -> rx.Component:
|
|
15
37
|
"""Compact file upload dropzone."""
|
|
@@ -463,6 +485,13 @@ def review_page() -> rx.Component:
|
|
|
463
485
|
"""Review and edit tax data page."""
|
|
464
486
|
return page_container(
|
|
465
487
|
api_modal(),
|
|
488
|
+
# Processing overlay - shows full screen modal during AI processing
|
|
489
|
+
processing_overlay(
|
|
490
|
+
is_processing=TaxAppState.processing,
|
|
491
|
+
current_file=TaxAppState.processing_current_file,
|
|
492
|
+
file_index=TaxAppState.processing_file_index,
|
|
493
|
+
total_files=TaxAppState.processing_total_files,
|
|
494
|
+
),
|
|
466
495
|
rx.vstack(
|
|
467
496
|
# Header
|
|
468
497
|
rx.hstack(
|
|
@@ -503,67 +532,97 @@ def review_page() -> rx.Component:
|
|
|
503
532
|
),
|
|
504
533
|
rx.divider(border_color=COLORS["border"]),
|
|
505
534
|
|
|
506
|
-
# File list with checkboxes
|
|
535
|
+
# File list with checkboxes - ENTIRE ROW CLICKABLE
|
|
507
536
|
rx.foreach(
|
|
508
537
|
TaxAppState.parsed_documents,
|
|
509
|
-
lambda doc: rx.
|
|
510
|
-
rx.
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
538
|
+
lambda doc: rx.box(
|
|
539
|
+
rx.hstack(
|
|
540
|
+
rx.cond(
|
|
541
|
+
doc["status"] == "parsed",
|
|
542
|
+
rx.icon("circle-check", size=18, color=COLORS["success"]),
|
|
543
|
+
rx.box(
|
|
544
|
+
rx.cond(
|
|
545
|
+
TaxAppState.selected_files.contains(doc["name"]),
|
|
546
|
+
rx.icon("square-check", size=20, color=COLORS["accent_primary"]),
|
|
547
|
+
rx.icon("square", size=20, color=COLORS["text_muted"]),
|
|
548
|
+
),
|
|
549
|
+
),
|
|
517
550
|
),
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
rx.text(
|
|
522
|
-
rx.cond(
|
|
523
|
-
doc["status"] == "parsed",
|
|
524
|
-
f"✓ Processed - {doc['type']}",
|
|
551
|
+
rx.vstack(
|
|
552
|
+
rx.text(doc["name"], color=COLORS["text_primary"], font_size="14px"),
|
|
553
|
+
rx.text(
|
|
525
554
|
rx.cond(
|
|
526
|
-
doc["status"] == "
|
|
527
|
-
"
|
|
555
|
+
doc["status"] == "parsed",
|
|
556
|
+
f"✓ Processed - {doc['type']}",
|
|
528
557
|
rx.cond(
|
|
529
|
-
doc["status"] == "
|
|
530
|
-
"
|
|
531
|
-
|
|
558
|
+
doc["status"] == "error",
|
|
559
|
+
"⚠ Error - click to retry",
|
|
560
|
+
rx.cond(
|
|
561
|
+
doc["status"] == "processing",
|
|
562
|
+
"⏳ Processing...",
|
|
563
|
+
f"Pending - {doc['type']}",
|
|
564
|
+
),
|
|
532
565
|
),
|
|
533
566
|
),
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
567
|
+
color=rx.cond(
|
|
568
|
+
doc["status"] == "parsed",
|
|
569
|
+
COLORS["success"],
|
|
570
|
+
rx.cond(
|
|
571
|
+
doc["status"] == "error",
|
|
572
|
+
COLORS["warning"],
|
|
573
|
+
COLORS["text_muted"],
|
|
574
|
+
),
|
|
542
575
|
),
|
|
576
|
+
font_size="12px",
|
|
543
577
|
),
|
|
544
|
-
|
|
578
|
+
align_items="start",
|
|
579
|
+
spacing="0",
|
|
580
|
+
flex="1",
|
|
545
581
|
),
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
582
|
+
rx.box(
|
|
583
|
+
rx.icon(
|
|
584
|
+
"x",
|
|
585
|
+
size=16,
|
|
586
|
+
color=COLORS["text_muted"],
|
|
587
|
+
_hover={"color": COLORS["error"]},
|
|
588
|
+
),
|
|
589
|
+
cursor="pointer",
|
|
590
|
+
padding="4px",
|
|
591
|
+
border_radius="4px",
|
|
592
|
+
_hover={"background": COLORS["error_light"]},
|
|
593
|
+
on_click=lambda: TaxAppState.remove_file(doc["name"]),
|
|
594
|
+
),
|
|
595
|
+
width="100%",
|
|
596
|
+
spacing="3",
|
|
557
597
|
),
|
|
558
|
-
|
|
559
|
-
padding="10px 12px",
|
|
598
|
+
padding="12px 16px",
|
|
560
599
|
background=rx.cond(
|
|
561
600
|
doc["status"] == "parsed",
|
|
562
601
|
COLORS["success_light"],
|
|
563
|
-
|
|
602
|
+
rx.cond(
|
|
603
|
+
TaxAppState.selected_files.contains(doc["name"]),
|
|
604
|
+
"#EEF2FF", # Light indigo when selected
|
|
605
|
+
COLORS["bg_hover"],
|
|
606
|
+
),
|
|
564
607
|
),
|
|
565
608
|
border_radius="8px",
|
|
566
|
-
|
|
609
|
+
border=rx.cond(
|
|
610
|
+
TaxAppState.selected_files.contains(doc["name"]),
|
|
611
|
+
f"2px solid {COLORS['accent_primary']}",
|
|
612
|
+
"2px solid transparent",
|
|
613
|
+
),
|
|
614
|
+
cursor=rx.cond(doc["status"] == "parsed", "default", "pointer"),
|
|
615
|
+
_hover=rx.cond(
|
|
616
|
+
doc["status"] != "parsed",
|
|
617
|
+
{"background": "#E8EDFF"},
|
|
618
|
+
{},
|
|
619
|
+
),
|
|
620
|
+
on_click=rx.cond(
|
|
621
|
+
doc["status"] != "parsed",
|
|
622
|
+
lambda: TaxAppState.toggle_file_selection(doc["name"]),
|
|
623
|
+
lambda: None,
|
|
624
|
+
),
|
|
625
|
+
transition="all 0.15s ease",
|
|
567
626
|
),
|
|
568
627
|
),
|
|
569
628
|
|
|
@@ -677,16 +736,54 @@ def review_page() -> rx.Component:
|
|
|
677
736
|
),
|
|
678
737
|
rx.cond(
|
|
679
738
|
TaxAppState.rental_properties.length() > 0,
|
|
680
|
-
rx.
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
rx.
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
739
|
+
rx.vstack(
|
|
740
|
+
rx.foreach(
|
|
741
|
+
TaxAppState.rental_properties,
|
|
742
|
+
lambda p, idx: rx.box(
|
|
743
|
+
rx.hstack(
|
|
744
|
+
rx.vstack(
|
|
745
|
+
rx.text(p["address"], color=COLORS["text_primary"], font_weight="600", font_size="15px"),
|
|
746
|
+
rx.hstack(
|
|
747
|
+
rx.text(f"Rent: ${p['monthly_rent']:,.0f}/mo", color=COLORS["accent_primary"], font_size="13px"),
|
|
748
|
+
rx.text("→", color=COLORS["text_muted"], font_size="13px"),
|
|
749
|
+
rx.text(f"${p['rent_income']:,.0f}/yr", color=COLORS["success"], font_size="13px", font_weight="500"),
|
|
750
|
+
spacing="1",
|
|
751
|
+
),
|
|
752
|
+
rx.text(f"Expenses: ${p['total_expenses']:,.0f}/yr",
|
|
753
|
+
color=COLORS["text_muted"], font_size="12px"),
|
|
754
|
+
align_items="start",
|
|
755
|
+
spacing="1",
|
|
756
|
+
flex="1",
|
|
757
|
+
),
|
|
758
|
+
rx.vstack(
|
|
759
|
+
rx.text("Net Income", color=COLORS["text_muted"], font_size="11px"),
|
|
760
|
+
rx.text(f"${p['net_income']:,.0f}",
|
|
761
|
+
color=rx.cond(p["net_income"].to(float) >= 0, COLORS["success"], COLORS["error"]),
|
|
762
|
+
font_weight="700",
|
|
763
|
+
font_size="18px"),
|
|
764
|
+
rx.text("/year", color=COLORS["text_muted"], font_size="11px"),
|
|
765
|
+
align_items="center",
|
|
766
|
+
spacing="0",
|
|
767
|
+
),
|
|
768
|
+
rx.box(
|
|
769
|
+
rx.icon("x", size=16, color=COLORS["text_muted"]),
|
|
770
|
+
cursor="pointer",
|
|
771
|
+
padding="6px",
|
|
772
|
+
border_radius="6px",
|
|
773
|
+
_hover={"background": COLORS["error_light"], "color": COLORS["error"]},
|
|
774
|
+
on_click=lambda: TaxAppState.remove_rental_property(idx),
|
|
775
|
+
),
|
|
776
|
+
width="100%", spacing="4", align_items="center",
|
|
777
|
+
),
|
|
778
|
+
padding="16px",
|
|
779
|
+
background="white",
|
|
780
|
+
border_radius="10px",
|
|
781
|
+
border=f"1px solid {COLORS['border']}",
|
|
782
|
+
box_shadow="0 1px 3px rgba(0,0,0,0.08)",
|
|
783
|
+
),
|
|
689
784
|
),
|
|
785
|
+
spacing="3",
|
|
786
|
+
width="100%",
|
|
690
787
|
),
|
|
691
788
|
rx.text("No rental properties added", color=COLORS["text_muted"], font_style="italic"),
|
|
692
789
|
),
|
|
@@ -694,30 +791,130 @@ def review_page() -> rx.Component:
|
|
|
694
791
|
rx.cond(
|
|
695
792
|
TaxAppState.show_rental_form,
|
|
696
793
|
rx.vstack(
|
|
697
|
-
rx.input(
|
|
794
|
+
rx.input(
|
|
795
|
+
placeholder="Address",
|
|
796
|
+
value=TaxAppState.rental_form_address,
|
|
797
|
+
on_change=TaxAppState.set_rental_address,
|
|
798
|
+
width="100%",
|
|
799
|
+
background="white",
|
|
800
|
+
border=f"1px solid {COLORS['border']}",
|
|
801
|
+
color=COLORS["text_primary"],
|
|
802
|
+
_placeholder={"color": COLORS["text_muted"]},
|
|
803
|
+
padding="10px 12px",
|
|
804
|
+
border_radius="8px",
|
|
805
|
+
_focus={"border_color": COLORS["accent_primary"], "box_shadow": f"0 0 0 3px {COLORS['accent_light']}"},
|
|
806
|
+
),
|
|
698
807
|
rx.hstack(
|
|
699
|
-
rx.input(
|
|
700
|
-
|
|
808
|
+
rx.input(
|
|
809
|
+
placeholder="Monthly Rent ($)",
|
|
810
|
+
value=TaxAppState.rental_form_income,
|
|
811
|
+
on_change=TaxAppState.set_rental_income,
|
|
812
|
+
type="number",
|
|
813
|
+
background="white",
|
|
814
|
+
border=f"1px solid {COLORS['border']}",
|
|
815
|
+
color=COLORS["text_primary"],
|
|
816
|
+
_placeholder={"color": COLORS["text_muted"]},
|
|
817
|
+
padding="10px 12px",
|
|
818
|
+
border_radius="8px",
|
|
819
|
+
_focus={"border_color": COLORS["accent_primary"], "box_shadow": f"0 0 0 3px {COLORS['accent_light']}"},
|
|
820
|
+
),
|
|
821
|
+
rx.input(
|
|
822
|
+
placeholder="Mortgage Interest ($)",
|
|
823
|
+
value=TaxAppState.rental_form_mortgage,
|
|
824
|
+
on_change=TaxAppState.set_rental_mortgage,
|
|
825
|
+
type="number",
|
|
826
|
+
background="white",
|
|
827
|
+
border=f"1px solid {COLORS['border']}",
|
|
828
|
+
color=COLORS["text_primary"],
|
|
829
|
+
_placeholder={"color": COLORS["text_muted"]},
|
|
830
|
+
padding="10px 12px",
|
|
831
|
+
border_radius="8px",
|
|
832
|
+
_focus={"border_color": COLORS["accent_primary"], "box_shadow": f"0 0 0 3px {COLORS['accent_light']}"},
|
|
833
|
+
),
|
|
701
834
|
width="100%",
|
|
835
|
+
spacing="3",
|
|
702
836
|
),
|
|
703
837
|
rx.hstack(
|
|
704
|
-
rx.input(
|
|
705
|
-
|
|
838
|
+
rx.input(
|
|
839
|
+
placeholder="Property Tax (Quarterly $)",
|
|
840
|
+
value=TaxAppState.rental_form_tax,
|
|
841
|
+
on_change=TaxAppState.set_rental_tax,
|
|
842
|
+
type="number",
|
|
843
|
+
background="white",
|
|
844
|
+
border=f"1px solid {COLORS['border']}",
|
|
845
|
+
color=COLORS["text_primary"],
|
|
846
|
+
_placeholder={"color": COLORS["text_muted"]},
|
|
847
|
+
padding="10px 12px",
|
|
848
|
+
border_radius="8px",
|
|
849
|
+
_focus={"border_color": COLORS["accent_primary"], "box_shadow": f"0 0 0 3px {COLORS['accent_light']}"},
|
|
850
|
+
),
|
|
851
|
+
rx.input(
|
|
852
|
+
placeholder="Insurance ($)",
|
|
853
|
+
value=TaxAppState.rental_form_insurance,
|
|
854
|
+
on_change=TaxAppState.set_rental_insurance,
|
|
855
|
+
type="number",
|
|
856
|
+
background="white",
|
|
857
|
+
border=f"1px solid {COLORS['border']}",
|
|
858
|
+
color=COLORS["text_primary"],
|
|
859
|
+
_placeholder={"color": COLORS["text_muted"]},
|
|
860
|
+
padding="10px 12px",
|
|
861
|
+
border_radius="8px",
|
|
862
|
+
_focus={"border_color": COLORS["accent_primary"], "box_shadow": f"0 0 0 3px {COLORS['accent_light']}"},
|
|
863
|
+
),
|
|
706
864
|
width="100%",
|
|
865
|
+
spacing="3",
|
|
707
866
|
),
|
|
708
867
|
rx.hstack(
|
|
709
|
-
rx.input(
|
|
710
|
-
|
|
868
|
+
rx.input(
|
|
869
|
+
placeholder="Repairs ($)",
|
|
870
|
+
value=TaxAppState.rental_form_repairs,
|
|
871
|
+
on_change=TaxAppState.set_rental_repairs,
|
|
872
|
+
type="number",
|
|
873
|
+
background="white",
|
|
874
|
+
border=f"1px solid {COLORS['border']}",
|
|
875
|
+
color=COLORS["text_primary"],
|
|
876
|
+
_placeholder={"color": COLORS["text_muted"]},
|
|
877
|
+
padding="10px 12px",
|
|
878
|
+
border_radius="8px",
|
|
879
|
+
_focus={"border_color": COLORS["accent_primary"], "box_shadow": f"0 0 0 3px {COLORS['accent_light']}"},
|
|
880
|
+
),
|
|
881
|
+
rx.input(
|
|
882
|
+
placeholder="Depreciation ($)",
|
|
883
|
+
value=TaxAppState.rental_form_depreciation,
|
|
884
|
+
on_change=TaxAppState.set_rental_depreciation,
|
|
885
|
+
type="number",
|
|
886
|
+
background="white",
|
|
887
|
+
border=f"1px solid {COLORS['border']}",
|
|
888
|
+
color=COLORS["text_primary"],
|
|
889
|
+
_placeholder={"color": COLORS["text_muted"]},
|
|
890
|
+
padding="10px 12px",
|
|
891
|
+
border_radius="8px",
|
|
892
|
+
_focus={"border_color": COLORS["accent_primary"], "box_shadow": f"0 0 0 3px {COLORS['accent_light']}"},
|
|
893
|
+
),
|
|
711
894
|
width="100%",
|
|
895
|
+
spacing="3",
|
|
896
|
+
),
|
|
897
|
+
rx.input(
|
|
898
|
+
placeholder="Other Expenses ($)",
|
|
899
|
+
value=TaxAppState.rental_form_other,
|
|
900
|
+
on_change=TaxAppState.set_rental_other,
|
|
901
|
+
type="number",
|
|
902
|
+
width="100%",
|
|
903
|
+
background="white",
|
|
904
|
+
border=f"1px solid {COLORS['border']}",
|
|
905
|
+
color=COLORS["text_primary"],
|
|
906
|
+
_placeholder={"color": COLORS["text_muted"]},
|
|
907
|
+
padding="10px 12px",
|
|
908
|
+
border_radius="8px",
|
|
909
|
+
_focus={"border_color": COLORS["accent_primary"], "box_shadow": f"0 0 0 3px {COLORS['accent_light']}"},
|
|
712
910
|
),
|
|
713
|
-
rx.input(placeholder="Other Expenses", value=TaxAppState.rental_form_other, on_change=TaxAppState.set_rental_other, type="number", width="100%"),
|
|
714
911
|
rx.hstack(
|
|
715
912
|
rx.button("Cancel", variant="outline", on_click=TaxAppState.toggle_rental_form),
|
|
716
913
|
rx.button("Add Property", on_click=TaxAppState.submit_rental_form),
|
|
717
|
-
width="100%", justify="end",
|
|
914
|
+
width="100%", justify="end", spacing="3",
|
|
718
915
|
),
|
|
719
|
-
width="100%", spacing="
|
|
720
|
-
background="
|
|
916
|
+
width="100%", spacing="3", padding="16px",
|
|
917
|
+
background=COLORS["bg_page"], border_radius="8px", border=f"1px solid {COLORS['border']}",
|
|
721
918
|
),
|
|
722
919
|
),
|
|
723
920
|
rx.text(f"Total Rental Income: ${TaxAppState.total_rental_income:,.2f}",
|
|
@@ -753,19 +950,20 @@ def review_page() -> rx.Component:
|
|
|
753
950
|
rx.cond(
|
|
754
951
|
TaxAppState.show_business_form,
|
|
755
952
|
rx.vstack(
|
|
756
|
-
|
|
953
|
+
styled_input("Business Name", TaxAppState.business_form_name, TaxAppState.set_business_name),
|
|
757
954
|
rx.hstack(
|
|
758
|
-
|
|
759
|
-
|
|
955
|
+
styled_input("Gross Income ($)", TaxAppState.business_form_income, TaxAppState.set_business_income, "number"),
|
|
956
|
+
styled_input("Total Expenses ($)", TaxAppState.business_form_expenses, TaxAppState.set_business_expenses, "number"),
|
|
760
957
|
width="100%",
|
|
958
|
+
spacing="3",
|
|
761
959
|
),
|
|
762
960
|
rx.hstack(
|
|
763
961
|
rx.button("Cancel", variant="outline", on_click=TaxAppState.toggle_business_form),
|
|
764
962
|
rx.button("Add Business", on_click=TaxAppState.submit_business_form),
|
|
765
|
-
width="100%", justify="end",
|
|
963
|
+
width="100%", justify="end", spacing="3",
|
|
766
964
|
),
|
|
767
|
-
width="100%", spacing="
|
|
768
|
-
background="
|
|
965
|
+
width="100%", spacing="3", padding="16px",
|
|
966
|
+
background=COLORS["bg_page"], border_radius="8px", border=f"1px solid {COLORS['border']}",
|
|
769
967
|
),
|
|
770
968
|
),
|
|
771
969
|
rx.text(f"Total Business Income: ${TaxAppState.total_business_income:,.2f}",
|
|
@@ -803,10 +1001,12 @@ def review_page() -> rx.Component:
|
|
|
803
1001
|
rx.cond(
|
|
804
1002
|
TaxAppState.show_other_income_form,
|
|
805
1003
|
rx.hstack(
|
|
806
|
-
|
|
807
|
-
|
|
1004
|
+
styled_input("Description", TaxAppState.other_income_form_desc, TaxAppState.set_other_income_desc),
|
|
1005
|
+
styled_input("Amount ($)", TaxAppState.other_income_form_amount, TaxAppState.set_other_income_amount, "number", "140px"),
|
|
808
1006
|
rx.button("Add", on_click=TaxAppState.submit_other_income_form),
|
|
809
1007
|
width="100%",
|
|
1008
|
+
spacing="3",
|
|
1009
|
+
padding="8px 0",
|
|
810
1010
|
),
|
|
811
1011
|
),
|
|
812
1012
|
width="100%", spacing="2", padding="12px",
|
|
@@ -837,10 +1037,12 @@ def review_page() -> rx.Component:
|
|
|
837
1037
|
rx.cond(
|
|
838
1038
|
TaxAppState.show_other_deduction_form,
|
|
839
1039
|
rx.hstack(
|
|
840
|
-
|
|
841
|
-
|
|
1040
|
+
styled_input("Description", TaxAppState.other_deduction_form_desc, TaxAppState.set_other_deduction_desc),
|
|
1041
|
+
styled_input("Amount ($)", TaxAppState.other_deduction_form_amount, TaxAppState.set_other_deduction_amount, "number", "140px"),
|
|
842
1042
|
rx.button("Add", on_click=TaxAppState.submit_other_deduction_form),
|
|
843
1043
|
width="100%",
|
|
1044
|
+
spacing="3",
|
|
1045
|
+
padding="8px 0",
|
|
844
1046
|
),
|
|
845
1047
|
),
|
|
846
1048
|
width="100%", spacing="2", padding="12px",
|
|
@@ -877,20 +1079,21 @@ def review_page() -> rx.Component:
|
|
|
877
1079
|
TaxAppState.show_dependent_form,
|
|
878
1080
|
rx.vstack(
|
|
879
1081
|
rx.hstack(
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1082
|
+
styled_input("Name", TaxAppState.dependent_form_name, TaxAppState.set_dependent_name),
|
|
1083
|
+
styled_input("Relationship", TaxAppState.dependent_form_relationship, TaxAppState.set_dependent_relationship, "text", "140px"),
|
|
1084
|
+
styled_input("Age", TaxAppState.dependent_form_age, TaxAppState.set_dependent_age, "number", "80px"),
|
|
883
1085
|
width="100%",
|
|
1086
|
+
spacing="3",
|
|
884
1087
|
),
|
|
885
1088
|
rx.text("Under 17 = $2,000 Child Tax Credit | 17+ = $500 Other Dependent Credit",
|
|
886
1089
|
color=COLORS["text_muted"], font_size="12px"),
|
|
887
1090
|
rx.hstack(
|
|
888
1091
|
rx.button("Cancel", variant="outline", on_click=TaxAppState.toggle_dependent_form),
|
|
889
1092
|
rx.button("Add Dependent", on_click=TaxAppState.submit_dependent_form),
|
|
890
|
-
width="100%", justify="end",
|
|
1093
|
+
width="100%", justify="end", spacing="3",
|
|
891
1094
|
),
|
|
892
|
-
width="100%", spacing="
|
|
893
|
-
background="
|
|
1095
|
+
width="100%", spacing="3", padding="16px",
|
|
1096
|
+
background=COLORS["bg_page"], border_radius="8px", border=f"1px solid {COLORS['border']}",
|
|
894
1097
|
),
|
|
895
1098
|
),
|
|
896
1099
|
# Credits Summary
|
|
@@ -1309,7 +1512,7 @@ app = rx.App(
|
|
|
1309
1512
|
)
|
|
1310
1513
|
|
|
1311
1514
|
# Register pages
|
|
1312
|
-
app.add_page(dashboard_page, route="/", title="TaxForge - Dashboard")
|
|
1515
|
+
app.add_page(dashboard_page, route="/", title="TaxForge - Dashboard", on_load=TaxAppState.load_saved_data)
|
|
1313
1516
|
app.add_page(upload_page, route="/upload", title="TaxForge - Upload")
|
|
1314
1517
|
app.add_page(review_page, route="/review", title="TaxForge - Review")
|
|
1315
1518
|
app.add_page(settings_page, route="/settings", title="TaxForge - Settings")
|
aitax/cli.py
CHANGED
aitax/components.py
CHANGED
|
@@ -562,3 +562,81 @@ def api_key_modal(is_open: bool, on_close: Callable, api_key_value: str,
|
|
|
562
562
|
),
|
|
563
563
|
),
|
|
564
564
|
)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def processing_overlay(
|
|
568
|
+
is_processing: bool,
|
|
569
|
+
current_file: str,
|
|
570
|
+
file_index: int,
|
|
571
|
+
total_files: int,
|
|
572
|
+
) -> rx.Component:
|
|
573
|
+
"""Full-screen processing overlay with progress."""
|
|
574
|
+
return rx.cond(
|
|
575
|
+
is_processing,
|
|
576
|
+
rx.box(
|
|
577
|
+
# Backdrop
|
|
578
|
+
rx.box(
|
|
579
|
+
position="fixed",
|
|
580
|
+
top="0",
|
|
581
|
+
left="0",
|
|
582
|
+
right="0",
|
|
583
|
+
bottom="0",
|
|
584
|
+
background="rgba(0, 0, 0, 0.5)",
|
|
585
|
+
z_index="300",
|
|
586
|
+
),
|
|
587
|
+
# Modal
|
|
588
|
+
rx.box(
|
|
589
|
+
rx.vstack(
|
|
590
|
+
rx.box(
|
|
591
|
+
rx.spinner(size="3", color=COLORS["accent_primary"]),
|
|
592
|
+
padding="16px",
|
|
593
|
+
background=COLORS["accent_light"],
|
|
594
|
+
border_radius="50%",
|
|
595
|
+
),
|
|
596
|
+
rx.text("Processing Documents",
|
|
597
|
+
color=COLORS["text_primary"],
|
|
598
|
+
font_size="20px",
|
|
599
|
+
font_weight="600"),
|
|
600
|
+
rx.text(f"File {file_index} of {total_files}",
|
|
601
|
+
color=COLORS["text_secondary"],
|
|
602
|
+
font_size="14px"),
|
|
603
|
+
rx.text(current_file,
|
|
604
|
+
color=COLORS["accent_primary"],
|
|
605
|
+
font_size="14px",
|
|
606
|
+
font_weight="500",
|
|
607
|
+
max_width="280px",
|
|
608
|
+
overflow="hidden",
|
|
609
|
+
text_overflow="ellipsis",
|
|
610
|
+
white_space="nowrap"),
|
|
611
|
+
rx.box(
|
|
612
|
+
rx.progress(
|
|
613
|
+
value=file_index,
|
|
614
|
+
max=total_files,
|
|
615
|
+
width="100%",
|
|
616
|
+
color_scheme="indigo",
|
|
617
|
+
),
|
|
618
|
+
width="100%",
|
|
619
|
+
padding="8px 0",
|
|
620
|
+
),
|
|
621
|
+
rx.text("Please wait while AI extracts data...",
|
|
622
|
+
color=COLORS["text_muted"],
|
|
623
|
+
font_size="13px"),
|
|
624
|
+
spacing="3",
|
|
625
|
+
align_items="center",
|
|
626
|
+
width="100%",
|
|
627
|
+
),
|
|
628
|
+
background="white",
|
|
629
|
+
border_radius="16px",
|
|
630
|
+
padding="32px",
|
|
631
|
+
box_shadow="0 25px 50px -12px rgba(0, 0, 0, 0.25)",
|
|
632
|
+
position="fixed",
|
|
633
|
+
top="50%",
|
|
634
|
+
left="50%",
|
|
635
|
+
transform="translate(-50%, -50%)",
|
|
636
|
+
z_index="301",
|
|
637
|
+
width="90%",
|
|
638
|
+
max_width="360px",
|
|
639
|
+
text_align="center",
|
|
640
|
+
),
|
|
641
|
+
),
|
|
642
|
+
)
|
aitax/document_extractor.py
CHANGED
|
@@ -10,9 +10,11 @@ from pathlib import Path
|
|
|
10
10
|
from typing import Optional
|
|
11
11
|
|
|
12
12
|
# Rate limiting settings
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
# Gemini free tier: 15 RPM (requests per minute) = 1 request per 4 seconds
|
|
14
|
+
# Being conservative with 5 seconds to avoid hitting limits
|
|
15
|
+
MAX_RETRIES = 5
|
|
16
|
+
RETRY_DELAY_SECONDS = 10 # Initial retry delay (will exponentially backoff)
|
|
17
|
+
REQUEST_DELAY_SECONDS = 5 # Delay between requests to avoid rate limiting
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
# Prompt to extract tax document data
|
|
@@ -36,12 +38,13 @@ For 1099-INT forms, extract:
|
|
|
36
38
|
- interest_income: Box 1 - Interest income
|
|
37
39
|
- federal_withheld: Box 4 - Federal income tax withheld
|
|
38
40
|
|
|
39
|
-
For 1099-DIV forms, extract:
|
|
40
|
-
- payer_name: Institution name
|
|
41
|
-
- ordinary_dividends: Box 1a - Total ordinary dividends
|
|
41
|
+
For 1099-DIV forms (Dividend Income), extract:
|
|
42
|
+
- payer_name: Institution/broker name (e.g., Fidelity, Schwab, Vanguard)
|
|
43
|
+
- ordinary_dividends: Box 1a - Total ordinary dividends (THIS IS THE MAIN AMOUNT - look for it carefully!)
|
|
42
44
|
- qualified_dividends: Box 1b - Qualified dividends
|
|
43
45
|
- capital_gain: Box 2a - Total capital gain distributions
|
|
44
46
|
- federal_withheld: Box 4 - Federal income tax withheld
|
|
47
|
+
NOTE: On consolidated 1099s, dividend info may be labeled "1099-DIV" in a section. Look for dollar amounts next to Box 1a.
|
|
45
48
|
|
|
46
49
|
For 1099-B forms, extract:
|
|
47
50
|
- payer_name: Broker name
|
|
@@ -268,28 +271,47 @@ async def extract_with_retry(
|
|
|
268
271
|
api_key: Optional[str] = None,
|
|
269
272
|
) -> dict:
|
|
270
273
|
"""
|
|
271
|
-
Extract with automatic retry on rate limiting.
|
|
274
|
+
Extract with automatic retry on rate limiting and transient errors.
|
|
272
275
|
"""
|
|
276
|
+
last_result = None
|
|
277
|
+
|
|
273
278
|
for attempt in range(MAX_RETRIES):
|
|
274
279
|
result = await extract_from_document(file_path, api_key)
|
|
280
|
+
last_result = result
|
|
281
|
+
|
|
282
|
+
error = result.get("error", "")
|
|
283
|
+
|
|
284
|
+
# Check for retryable errors
|
|
285
|
+
is_rate_limited = error == "RATE_LIMITED"
|
|
286
|
+
is_transient = any(x in str(error).lower() for x in [
|
|
287
|
+
"rate", "quota", "limit", "429", "500", "502", "503", "504",
|
|
288
|
+
"timeout", "overloaded", "capacity", "try again"
|
|
289
|
+
])
|
|
275
290
|
|
|
276
|
-
if
|
|
291
|
+
if is_rate_limited or is_transient:
|
|
277
292
|
if attempt < MAX_RETRIES - 1:
|
|
278
293
|
# Wait and retry with exponential backoff
|
|
279
294
|
wait_time = RETRY_DELAY_SECONDS * (2 ** attempt)
|
|
295
|
+
print(f"[TaxForge] Rate limited, waiting {wait_time}s before retry {attempt + 2}/{MAX_RETRIES}...")
|
|
280
296
|
await asyncio.sleep(wait_time)
|
|
281
297
|
continue
|
|
282
298
|
else:
|
|
283
299
|
return {
|
|
284
|
-
"error": "API quota exceeded after retries. Please wait a minute and try again.",
|
|
300
|
+
"error": f"API quota exceeded after {MAX_RETRIES} retries. Please wait a minute and try again.",
|
|
285
301
|
"document_type": "UNKNOWN",
|
|
286
302
|
"confidence": 0.0,
|
|
287
303
|
"extracted_data": {}
|
|
288
304
|
}
|
|
289
305
|
|
|
306
|
+
# Non-retryable error or success
|
|
290
307
|
return result
|
|
291
308
|
|
|
292
|
-
return
|
|
309
|
+
return last_result or {
|
|
310
|
+
"error": "Max retries exceeded",
|
|
311
|
+
"document_type": "UNKNOWN",
|
|
312
|
+
"confidence": 0.0,
|
|
313
|
+
"extracted_data": {}
|
|
314
|
+
}
|
|
293
315
|
|
|
294
316
|
|
|
295
317
|
def convert_to_w2(extracted_data: dict) -> dict:
|
|
@@ -304,17 +326,32 @@ def convert_to_w2(extracted_data: dict) -> dict:
|
|
|
304
326
|
def convert_to_1099(extracted_data: dict, form_type: str) -> dict:
|
|
305
327
|
"""Convert extracted data to 1099 format for state."""
|
|
306
328
|
amount = 0.0
|
|
329
|
+
|
|
307
330
|
if form_type == "1099-INT":
|
|
308
|
-
|
|
331
|
+
# Try multiple field names that LLM might use
|
|
332
|
+
amount = float(extracted_data.get("interest_income", 0) or
|
|
333
|
+
extracted_data.get("interest", 0) or
|
|
334
|
+
extracted_data.get("box_1", 0) or
|
|
335
|
+
extracted_data.get("taxable_interest", 0) or 0)
|
|
309
336
|
elif form_type == "1099-DIV":
|
|
310
|
-
|
|
337
|
+
# Try multiple field names that LLM might use
|
|
338
|
+
amount = float(extracted_data.get("ordinary_dividends", 0) or
|
|
339
|
+
extracted_data.get("total_ordinary_dividends", 0) or
|
|
340
|
+
extracted_data.get("dividends", 0) or
|
|
341
|
+
extracted_data.get("box_1a", 0) or
|
|
342
|
+
extracted_data.get("total_dividends", 0) or 0)
|
|
311
343
|
elif form_type == "1099-B":
|
|
312
|
-
|
|
344
|
+
# Try multiple field names
|
|
345
|
+
amount = float(extracted_data.get("gain_loss", 0) or
|
|
346
|
+
extracted_data.get("net_gain_loss", 0) or
|
|
347
|
+
extracted_data.get("proceeds", 0) or
|
|
348
|
+
extracted_data.get("total_proceeds", 0) or 0)
|
|
313
349
|
|
|
314
350
|
return {
|
|
315
|
-
"payer_name": extracted_data.get("payer_name", extracted_data.get("lender_name", "Unknown Payer")),
|
|
351
|
+
"payer_name": extracted_data.get("payer_name", extracted_data.get("lender_name", extracted_data.get("institution", "Unknown Payer"))),
|
|
316
352
|
"amount": amount,
|
|
317
353
|
"form_type": form_type,
|
|
354
|
+
"raw_data": extracted_data, # Keep raw for debugging
|
|
318
355
|
}
|
|
319
356
|
|
|
320
357
|
|
aitax/persistence.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TaxForge - Data Persistence
|
|
3
|
+
Save and load tax data to/from local JSON file.
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, Any, Optional
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
import base64
|
|
11
|
+
import hashlib
|
|
12
|
+
|
|
13
|
+
# Data directory
|
|
14
|
+
DATA_DIR = Path.home() / ".taxforge"
|
|
15
|
+
DATA_FILE = DATA_DIR / "tax_data.json"
|
|
16
|
+
BACKUP_DIR = DATA_DIR / "backups"
|
|
17
|
+
|
|
18
|
+
# Fields to persist (from TaxAppState)
|
|
19
|
+
PERSIST_FIELDS = [
|
|
20
|
+
# Personal info
|
|
21
|
+
"first_name", "last_name", "filing_status",
|
|
22
|
+
# API key (will be obfuscated)
|
|
23
|
+
"openai_api_key",
|
|
24
|
+
# Documents
|
|
25
|
+
"uploaded_files", "parsed_documents",
|
|
26
|
+
# Tax data
|
|
27
|
+
"w2_list", "form_1099_list", "form_1098_list", "form_5498_list",
|
|
28
|
+
# Manual entries
|
|
29
|
+
"rental_properties", "business_income", "other_income", "other_deductions",
|
|
30
|
+
"dependents", "child_care_expenses",
|
|
31
|
+
# Calculated values we want to persist
|
|
32
|
+
"total_wages", "total_interest", "total_dividends", "total_capital_gains",
|
|
33
|
+
"total_rental_income", "total_business_income", "total_other_income",
|
|
34
|
+
"hsa_deduction", "self_employment_tax", "mortgage_interest_deduction",
|
|
35
|
+
"adjusted_gross_income", "itemized_deductions", "standard_deduction",
|
|
36
|
+
"total_deductions", "taxable_income", "total_tax", "total_withholding",
|
|
37
|
+
"other_withholding", "refund_or_owed", "is_refund",
|
|
38
|
+
"child_tax_credit", "other_dependent_credit", "child_care_credit", "total_credits",
|
|
39
|
+
# Status
|
|
40
|
+
"return_status", "return_generated",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
# Fields that should be obfuscated (not encrypted, just not plain text)
|
|
44
|
+
SENSITIVE_FIELDS = ["openai_api_key"]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _obfuscate(value: str) -> str:
|
|
48
|
+
"""Simple obfuscation for sensitive values (not real encryption)."""
|
|
49
|
+
if not value:
|
|
50
|
+
return ""
|
|
51
|
+
return base64.b64encode(value.encode()).decode()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _deobfuscate(value: str) -> str:
|
|
55
|
+
"""Reverse obfuscation."""
|
|
56
|
+
if not value:
|
|
57
|
+
return ""
|
|
58
|
+
try:
|
|
59
|
+
return base64.b64decode(value.encode()).decode()
|
|
60
|
+
except:
|
|
61
|
+
return value # Return as-is if can't decode
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def ensure_data_dir():
|
|
65
|
+
"""Ensure data directory exists."""
|
|
66
|
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def save_state(state) -> bool:
|
|
71
|
+
"""
|
|
72
|
+
Save state to JSON file.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
state: TaxAppState instance
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
True if successful, False otherwise
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
ensure_data_dir()
|
|
82
|
+
|
|
83
|
+
# Extract fields to save
|
|
84
|
+
data = {
|
|
85
|
+
"_saved_at": datetime.now().isoformat(),
|
|
86
|
+
"_version": "0.9.21",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for field in PERSIST_FIELDS:
|
|
90
|
+
if hasattr(state, field):
|
|
91
|
+
value = getattr(state, field)
|
|
92
|
+
# Handle special types
|
|
93
|
+
if isinstance(value, (list, dict)):
|
|
94
|
+
data[field] = value
|
|
95
|
+
else:
|
|
96
|
+
data[field] = value
|
|
97
|
+
|
|
98
|
+
# Obfuscate sensitive fields
|
|
99
|
+
if field in SENSITIVE_FIELDS and value:
|
|
100
|
+
data[field] = _obfuscate(str(value))
|
|
101
|
+
|
|
102
|
+
# Create backup of existing file
|
|
103
|
+
if DATA_FILE.exists():
|
|
104
|
+
backup_name = f"tax_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
|
105
|
+
backup_path = BACKUP_DIR / backup_name
|
|
106
|
+
# Keep only last 5 backups
|
|
107
|
+
existing_backups = sorted(BACKUP_DIR.glob("tax_data_*.json"))
|
|
108
|
+
if len(existing_backups) >= 5:
|
|
109
|
+
for old_backup in existing_backups[:-4]:
|
|
110
|
+
old_backup.unlink()
|
|
111
|
+
|
|
112
|
+
# Write to file
|
|
113
|
+
with open(DATA_FILE, 'w') as f:
|
|
114
|
+
json.dump(data, f, indent=2, default=str)
|
|
115
|
+
|
|
116
|
+
return True
|
|
117
|
+
|
|
118
|
+
except Exception as e:
|
|
119
|
+
print(f"[TaxForge] Error saving state: {e}")
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def load_state(state) -> bool:
|
|
124
|
+
"""
|
|
125
|
+
Load state from JSON file.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
state: TaxAppState instance to populate
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
True if data was loaded, False if no data or error
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
if not DATA_FILE.exists():
|
|
135
|
+
print("[TaxForge] No saved data found, starting fresh")
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
with open(DATA_FILE, 'r') as f:
|
|
139
|
+
data = json.load(f)
|
|
140
|
+
|
|
141
|
+
print(f"[TaxForge] Loading saved data from {data.get('_saved_at', 'unknown')}")
|
|
142
|
+
|
|
143
|
+
# Restore fields
|
|
144
|
+
for field in PERSIST_FIELDS:
|
|
145
|
+
if field in data:
|
|
146
|
+
value = data[field]
|
|
147
|
+
|
|
148
|
+
# Deobfuscate sensitive fields
|
|
149
|
+
if field in SENSITIVE_FIELDS and value:
|
|
150
|
+
value = _deobfuscate(value)
|
|
151
|
+
|
|
152
|
+
# Set the attribute
|
|
153
|
+
if hasattr(state, field):
|
|
154
|
+
setattr(state, field, value)
|
|
155
|
+
|
|
156
|
+
print(f"[TaxForge] Loaded: {len(data.get('w2_list', []))} W-2s, "
|
|
157
|
+
f"{len(data.get('form_1099_list', []))} 1099s, "
|
|
158
|
+
f"{len(data.get('rental_properties', []))} rentals")
|
|
159
|
+
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
except Exception as e:
|
|
163
|
+
print(f"[TaxForge] Error loading state: {e}")
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def clear_saved_data() -> bool:
|
|
168
|
+
"""Clear all saved data."""
|
|
169
|
+
try:
|
|
170
|
+
if DATA_FILE.exists():
|
|
171
|
+
DATA_FILE.unlink()
|
|
172
|
+
return True
|
|
173
|
+
except Exception as e:
|
|
174
|
+
print(f"[TaxForge] Error clearing data: {e}")
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def get_data_info() -> Dict[str, Any]:
|
|
179
|
+
"""Get info about saved data."""
|
|
180
|
+
info = {
|
|
181
|
+
"exists": DATA_FILE.exists(),
|
|
182
|
+
"path": str(DATA_FILE),
|
|
183
|
+
"size": 0,
|
|
184
|
+
"saved_at": None,
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if DATA_FILE.exists():
|
|
188
|
+
info["size"] = DATA_FILE.stat().st_size
|
|
189
|
+
try:
|
|
190
|
+
with open(DATA_FILE, 'r') as f:
|
|
191
|
+
data = json.load(f)
|
|
192
|
+
info["saved_at"] = data.get("_saved_at")
|
|
193
|
+
info["version"] = data.get("_version")
|
|
194
|
+
except:
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
return info
|
aitax/state.py
CHANGED
|
@@ -15,6 +15,7 @@ from .document_extractor import (
|
|
|
15
15
|
convert_1099_misc_to_other_income,
|
|
16
16
|
REQUEST_DELAY_SECONDS
|
|
17
17
|
)
|
|
18
|
+
from .persistence import save_state, load_state, clear_saved_data
|
|
18
19
|
import asyncio
|
|
19
20
|
|
|
20
21
|
|
|
@@ -269,6 +270,7 @@ class TaxAppState(rx.State):
|
|
|
269
270
|
self.openai_api_key = self.temp_api_key
|
|
270
271
|
self.show_api_modal = False
|
|
271
272
|
self.success_message = "API key saved!"
|
|
273
|
+
self._auto_save() # Persist API key
|
|
272
274
|
|
|
273
275
|
def check_api_key_for_upload(self):
|
|
274
276
|
"""Check if API key is set, open modal if not."""
|
|
@@ -328,6 +330,7 @@ class TaxAppState(rx.State):
|
|
|
328
330
|
if new_files > 0:
|
|
329
331
|
self.success_message = f"Uploaded {new_files} file(s). Go to Review to process with AI."
|
|
330
332
|
self.return_status = "in_progress"
|
|
333
|
+
self._auto_save() # Persist uploaded files list
|
|
331
334
|
except Exception as e:
|
|
332
335
|
self.error_message = f"Upload failed: {str(e)}"
|
|
333
336
|
|
|
@@ -573,15 +576,18 @@ class TaxAppState(rx.State):
|
|
|
573
576
|
mortgage_interest: float = 0, property_tax: float = 0,
|
|
574
577
|
insurance: float = 0, repairs: float = 0,
|
|
575
578
|
management: float = 0, utilities: float = 0,
|
|
576
|
-
depreciation: float = 0, other_expenses: float = 0
|
|
577
|
-
|
|
579
|
+
depreciation: float = 0, other_expenses: float = 0,
|
|
580
|
+
monthly_rent: float = 0, quarterly_tax: float = 0):
|
|
581
|
+
"""Add a rental property (Schedule E). All values should be annual."""
|
|
578
582
|
total_expenses = (mortgage_interest + property_tax + insurance +
|
|
579
583
|
repairs + management + utilities + depreciation + other_expenses)
|
|
580
584
|
self.rental_properties.append({
|
|
581
585
|
"address": address,
|
|
582
|
-
"
|
|
586
|
+
"monthly_rent": monthly_rent, # For display
|
|
587
|
+
"quarterly_tax": quarterly_tax, # For display
|
|
588
|
+
"rent_income": rent_income, # Annual (monthly × 12)
|
|
583
589
|
"mortgage_interest": mortgage_interest,
|
|
584
|
-
"property_tax": property_tax,
|
|
590
|
+
"property_tax": property_tax, # Annual (quarterly × 4)
|
|
585
591
|
"insurance": insurance,
|
|
586
592
|
"repairs": repairs,
|
|
587
593
|
"management": management,
|
|
@@ -712,15 +718,25 @@ class TaxAppState(rx.State):
|
|
|
712
718
|
def submit_rental_form(self):
|
|
713
719
|
"""Submit rental property form."""
|
|
714
720
|
try:
|
|
721
|
+
# Monthly rent × 12 = Annual rent income
|
|
722
|
+
monthly_rent = float(self.rental_form_income or 0)
|
|
723
|
+
annual_rent = monthly_rent * 12
|
|
724
|
+
|
|
725
|
+
# Quarterly property tax × 4 = Annual
|
|
726
|
+
quarterly_tax = float(self.rental_form_tax or 0)
|
|
727
|
+
annual_tax = quarterly_tax * 4
|
|
728
|
+
|
|
715
729
|
self.add_rental_property(
|
|
716
730
|
address=self.rental_form_address or "Property",
|
|
717
|
-
rent_income=
|
|
731
|
+
rent_income=annual_rent,
|
|
718
732
|
mortgage_interest=float(self.rental_form_mortgage or 0),
|
|
719
|
-
property_tax=
|
|
733
|
+
property_tax=annual_tax,
|
|
720
734
|
insurance=float(self.rental_form_insurance or 0),
|
|
721
735
|
repairs=float(self.rental_form_repairs or 0),
|
|
722
736
|
depreciation=float(self.rental_form_depreciation or 0),
|
|
723
737
|
other_expenses=float(self.rental_form_other or 0),
|
|
738
|
+
monthly_rent=monthly_rent, # Store for display
|
|
739
|
+
quarterly_tax=quarterly_tax, # Store for display
|
|
724
740
|
)
|
|
725
741
|
# Clear form
|
|
726
742
|
self.rental_form_address = ""
|
|
@@ -1073,6 +1089,16 @@ class TaxAppState(rx.State):
|
|
|
1073
1089
|
|
|
1074
1090
|
if self.total_wages > 0 or total_income > 0:
|
|
1075
1091
|
self.return_status = "in_progress"
|
|
1092
|
+
|
|
1093
|
+
# Auto-save after recalculation
|
|
1094
|
+
self._auto_save()
|
|
1095
|
+
|
|
1096
|
+
def _auto_save(self):
|
|
1097
|
+
"""Auto-save state to disk."""
|
|
1098
|
+
try:
|
|
1099
|
+
save_state(self)
|
|
1100
|
+
except Exception as e:
|
|
1101
|
+
print(f"[TaxForge] Auto-save failed: {e}")
|
|
1076
1102
|
|
|
1077
1103
|
def _calculate_tax(self, income: float, brackets: list) -> float:
|
|
1078
1104
|
"""Calculate tax from brackets."""
|
|
@@ -1140,6 +1166,15 @@ class TaxAppState(rx.State):
|
|
|
1140
1166
|
self.return_generated = False
|
|
1141
1167
|
self.error_message = ""
|
|
1142
1168
|
self.success_message = ""
|
|
1169
|
+
# Also clear persisted data
|
|
1170
|
+
clear_saved_data()
|
|
1171
|
+
|
|
1172
|
+
def load_saved_data(self):
|
|
1173
|
+
"""Load previously saved data from disk."""
|
|
1174
|
+
if load_state(self):
|
|
1175
|
+
self.success_message = "Previous session restored!"
|
|
1176
|
+
else:
|
|
1177
|
+
self.success_message = ""
|
|
1143
1178
|
|
|
1144
1179
|
# ===== Computed Properties =====
|
|
1145
1180
|
@rx.var
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
aitax/__init__.py,sha256=nyfwdgcJkm0IZZxEUPth1pyolrF5U_FCMcunvZ63w6k,68
|
|
2
|
+
aitax/aitax.py,sha256=6XY7MM5iKMh9PWpjajIwh5fDMR-zjLGXBn8Vjk9w9ZY,75937
|
|
3
|
+
aitax/cli.py,sha256=rTFcb_CCd0cM_KJjolruBk_2aH0k8MJrnRpyowkBEFE,2651
|
|
4
|
+
aitax/components.py,sha256=p3MQgKQr95zmO5Uu-jcgCBApuULoV_AD2SZvAGGT0dw,23855
|
|
5
|
+
aitax/document_extractor.py,sha256=k1mEXNp5nxM7jSLi7_okZuElu6PItrKL7K2yHRpOc6c,16130
|
|
6
|
+
aitax/persistence.py,sha256=m9Bel2wrgDaduNQyRxyfXRYEsw_z_83xc2oK2I94FXI,5932
|
|
7
|
+
aitax/state.py,sha256=D85kA3mcUUP7YuDARfstTPjSPilVv2HAKDYD3K2jIAI,47234
|
|
8
|
+
taxforge-0.9.22.dist-info/METADATA,sha256=_DcfKQ9V9ze5feHr8D2bla7pEPoSbgGkfsDInYLBIDA,4874
|
|
9
|
+
taxforge-0.9.22.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
10
|
+
taxforge-0.9.22.dist-info/entry_points.txt,sha256=2dG4U_m3yvQaVeD8LeEGm1v8szyNGQtXUZOvbwH75Gg,44
|
|
11
|
+
taxforge-0.9.22.dist-info/top_level.txt,sha256=H7DuLZSRzSwHT5STc7uQa0oTOsKcNYUz2Pn4a4C5Q0Y,6
|
|
12
|
+
taxforge-0.9.22.dist-info/RECORD,,
|
taxforge-0.9.20.dist-info/RECORD
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
aitax/__init__.py,sha256=hV9XKDmqYNgMHDYg2q_S1YEwZ4owPZh93d_XtJ8_PVM,68
|
|
2
|
-
aitax/aitax.py,sha256=l0lSEz81q8A2__gpmmIFvn-8eol1cSFU8QQzhNtrbcU,63645
|
|
3
|
-
aitax/cli.py,sha256=dm3M-sQ506Q5_hJRh_fOeosPF-ufY43BT46AeeOUwJU,2651
|
|
4
|
-
aitax/components.py,sha256=pi4wKOtNsw0ju1cN3ivY7UWF0Q32rshijeklFnxVt5U,21056
|
|
5
|
-
aitax/document_extractor.py,sha256=gArprV9JkIXCUsjUdbmbBobcAIzhHPZMcpUY4VKcGtA,14104
|
|
6
|
-
aitax/state.py,sha256=vr5hlK2Dv3a4fhMntK6NUTnQdBjILpewoWGaq8DRQAw,45779
|
|
7
|
-
taxforge-0.9.20.dist-info/METADATA,sha256=YGHJYtiUf7B6d7v8TBUo3gmCaoxOlknUkdJhSbdeFOg,4874
|
|
8
|
-
taxforge-0.9.20.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
9
|
-
taxforge-0.9.20.dist-info/entry_points.txt,sha256=2dG4U_m3yvQaVeD8LeEGm1v8szyNGQtXUZOvbwH75Gg,44
|
|
10
|
-
taxforge-0.9.20.dist-info/top_level.txt,sha256=H7DuLZSRzSwHT5STc7uQa0oTOsKcNYUz2Pn4a4C5Q0Y,6
|
|
11
|
-
taxforge-0.9.20.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|