diff --git a/efile_app/efile/data/massachusetts_lower_courts.json b/efile_app/efile/data/massachusetts_lower_courts.json new file mode 100644 index 00000000..d1f0d78b --- /dev/null +++ b/efile_app/efile/data/massachusetts_lower_courts.json @@ -0,0 +1,1497 @@ +{ + "source": { + "repository": "https://github.com/SuffolkLITLab/docassemble-MACourts", + "commit": "ce671c33db2a98e1169dc300f8f685e8c93685ff", + "directory": "docassemble/MACourts/data/sources", + "files": [ + "bmc.json", + "district_courts.json", + "housing_courts.json", + "juvenile_courts.json", + "land_court.json", + "probate_and_family_courts.json", + "superior_courts.json" + ], + "note": "Records are grouped by identical stage and production lower-court codes. `source_names` and `cities` preserve the upstream court/session distinctions; displayed names clarify when a single Tyler code covers multiple locations. Rows without both codes are omitted." + }, + "courts": [ + { + "name": "Attleboro District Court", + "tyler_lower_court_code": "1764", + "tyler_prod_lower_court_code": "6993", + "source_names": [ + "Attleboro District Court" + ], + "cities": [ + "Attleboro" + ] + }, + { + "name": "Attleboro Juvenile Court", + "tyler_lower_court_code": "1839", + "tyler_prod_lower_court_code": "7068", + "source_names": [ + "Attleboro Juvenile Court" + ], + "cities": [ + "Attleboro" + ] + }, + { + "name": "Ayer District Court", + "tyler_lower_court_code": "1765", + "tyler_prod_lower_court_code": "6994", + "source_names": [ + "Ayer District Court" + ], + "cities": [ + "Ayer" + ] + }, + { + "name": "Barnstable County Superior Court", + "tyler_lower_court_code": "1870", + "tyler_prod_lower_court_code": "7099", + "source_names": [ + "Barnstable County Superior Court" + ], + "cities": [ + "Barnstable" + ] + }, + { + "name": "Barnstable District Court", + "tyler_lower_court_code": "1766", + "tyler_prod_lower_court_code": "6995", + "source_names": [ + "Barnstable District Court" + ], + "cities": [ + "Barnstable" + ] + }, + { + "name": "Barnstable Juvenile Court", + "tyler_lower_court_code": "1832", + "tyler_prod_lower_court_code": "7061", + "source_names": [ + "Barnstable Juvenile Court" + ], + "cities": [ + "Barnstable" + ] + }, + { + "name": "Barnstable Probate and Family Court", + "tyler_lower_court_code": "1855", + "tyler_prod_lower_court_code": "7084", + "source_names": [ + "Barnstable Probate and Family Court" + ], + "cities": [ + "Barnstable" + ] + }, + { + "name": "Berkshire County Superior Court", + "tyler_lower_court_code": "1871", + "tyler_prod_lower_court_code": "7100", + "source_names": [ + "Berkshire County Superior Court" + ], + "cities": [ + "Pittsfield" + ] + }, + { + "name": "Berkshire Probate and Family Court", + "tyler_lower_court_code": "1856", + "tyler_prod_lower_court_code": "7085", + "source_names": [ + "Berkshire Probate and Family Court" + ], + "cities": [ + "Pittsfield" + ] + }, + { + "name": "Brighton Division, Boston Municipal Court", + "tyler_lower_court_code": "1753", + "tyler_prod_lower_court_code": "6982", + "source_names": [ + "Brighton Division, Boston Municipal Court" + ], + "cities": [ + "Brighton" + ] + }, + { + "name": "Bristol County Superior Court (Fall River, New Bedford, Taunton)", + "tyler_lower_court_code": "1872", + "tyler_prod_lower_court_code": "7101", + "source_names": [ + "Bristol County Superior Court" + ], + "cities": [ + "Fall River", + "New Bedford", + "Taunton" + ] + }, + { + "name": "Brockton District Court", + "tyler_lower_court_code": "1767", + "tyler_prod_lower_court_code": "6996", + "source_names": [ + "Brockton District Court" + ], + "cities": [ + "Brockton" + ] + }, + { + "name": "Brockton Juvenile Court", + "tyler_lower_court_code": "1849", + "tyler_prod_lower_court_code": "7078", + "source_names": [ + "Brockton Juvenile Court" + ], + "cities": [ + "Brockton" + ] + }, + { + "name": "Brookline District Court", + "tyler_lower_court_code": "1768", + "tyler_prod_lower_court_code": "6997", + "source_names": [ + "Brookline District Court" + ], + "cities": [ + "Brookline" + ] + }, + { + "name": "Cambridge District Court", + "tyler_lower_court_code": "1769", + "tyler_prod_lower_court_code": "6998", + "source_names": [ + "Cambridge District Court" + ], + "cities": [ + "Medford" + ] + }, + { + "name": "Central Division, Boston Municipal Court", + "tyler_lower_court_code": "1754", + "tyler_prod_lower_court_code": "6983", + "source_names": [ + "Central Division, Boston Municipal Court" + ], + "cities": [ + "Edward W. Brooke Courthouse, Boston" + ] + }, + { + "name": "Central Housing Court (Dudley, Leominster, Marlborough, Worcester sessions)", + "tyler_lower_court_code": "1831", + "tyler_prod_lower_court_code": "7060", + "source_names": [ + "Central Housing Court - Dudley Session", + "Central Housing Court - Leominster Session", + "Central Housing Court - Marlborough Session", + "Central Housing Court - Worcester Session" + ], + "cities": [ + "Dudley District Court, Dudley", + "Leominster District Court, Leominster", + "Marlborough District Court, Marlborough", + "Worcester" + ] + }, + { + "name": "Charlestown Division, Boston Municipal Court", + "tyler_lower_court_code": "1755", + "tyler_prod_lower_court_code": "6984", + "source_names": [ + "Charlestown Division, Boston Municipal Court" + ], + "cities": [ + "Charlestown" + ] + }, + { + "name": "Chelsea District Court", + "tyler_lower_court_code": "1770", + "tyler_prod_lower_court_code": "6999", + "source_names": [ + "Chelsea District Court" + ], + "cities": [ + "Chelsea" + ] + }, + { + "name": "Chicopee District Court", + "tyler_lower_court_code": "1771", + "tyler_prod_lower_court_code": "7000", + "source_names": [ + "Chicopee District Court" + ], + "cities": [ + "Chicopee" + ] + }, + { + "name": "Clinton District Court", + "tyler_lower_court_code": "1772", + "tyler_prod_lower_court_code": "7001", + "source_names": [ + "Clinton District Court" + ], + "cities": [ + "Clinton" + ] + }, + { + "name": "Concord District Court", + "tyler_lower_court_code": "1773", + "tyler_prod_lower_court_code": "7002", + "source_names": [ + "Concord District Court" + ], + "cities": [ + "Concord" + ] + }, + { + "name": "Dedham District Court", + "tyler_lower_court_code": "1774", + "tyler_prod_lower_court_code": "7003", + "source_names": [ + "Dedham District Court" + ], + "cities": [ + "Dedham" + ] + }, + { + "name": "Dorchester Division, Boston Municipal Court", + "tyler_lower_court_code": "1756", + "tyler_prod_lower_court_code": "6985", + "source_names": [ + "Dorchester Division, Boston Municipal Court" + ], + "cities": [ + "Dorchester" + ] + }, + { + "name": "Dudley District Court", + "tyler_lower_court_code": "1775", + "tyler_prod_lower_court_code": "7004", + "source_names": [ + "Dudley District Court" + ], + "cities": [ + "Dudley" + ] + }, + { + "name": "Dukes County Superior Court", + "tyler_lower_court_code": "1873", + "tyler_prod_lower_court_code": "7102", + "source_names": [ + "Dukes County Superior Court" + ], + "cities": [ + "Edgartown" + ] + }, + { + "name": "Dukes Probate and Family Court", + "tyler_lower_court_code": "1858", + "tyler_prod_lower_court_code": "7087", + "source_names": [ + "Dukes Probate and Family Court" + ], + "cities": [ + "Edgartown" + ] + }, + { + "name": "East Boston Division, Boston Municipal Court", + "tyler_lower_court_code": "1757", + "tyler_prod_lower_court_code": "6986", + "source_names": [ + "East Boston Division, Boston Municipal Court" + ], + "cities": [ + "East Boston" + ] + }, + { + "name": "East Brookfield District Court", + "tyler_lower_court_code": "1776", + "tyler_prod_lower_court_code": "7005", + "source_names": [ + "East Brookfield District Court" + ], + "cities": [ + "East Brookfield" + ] + }, + { + "name": "Eastern Hampshire District Court", + "tyler_lower_court_code": "1777", + "tyler_prod_lower_court_code": "7006", + "source_names": [ + "Eastern Hampshire District Court" + ], + "cities": [ + "Belchertown" + ] + }, + { + "name": "Eastern Housing Court (including Chelsea and Middlesex sessions)", + "tyler_lower_court_code": "1827", + "tyler_prod_lower_court_code": "7056", + "source_names": [ + "Eastern Housing Court", + "Eastern Housing Court - Chelsea Session", + "Eastern Housing Court - Middlesex Session" + ], + "cities": [ + "Boston", + "Chelsea", + "Somerville" + ] + }, + { + "name": "Edgartown District Court", + "tyler_lower_court_code": "1778", + "tyler_prod_lower_court_code": "7007", + "source_names": [ + "Edgartown District Court" + ], + "cities": [ + "Edgartown" + ] + }, + { + "name": "Edgartown Juvenile Court", + "tyler_lower_court_code": "1833", + "tyler_prod_lower_court_code": "7062", + "source_names": [ + "Edgartown Juvenile Court" + ], + "cities": [ + "Edgartown" + ] + }, + { + "name": "Essex County Superior Court (Lawrence, Newburyport, Salem)", + "tyler_lower_court_code": "1874", + "tyler_prod_lower_court_code": "7103", + "source_names": [ + "Essex County Superior Court" + ], + "cities": [ + "Lawrence", + "Newburyport", + "Salem" + ] + }, + { + "name": "Fall River District Court", + "tyler_lower_court_code": "1779", + "tyler_prod_lower_court_code": "7008", + "source_names": [ + "Fall River District Court" + ], + "cities": [ + "Fall River" + ] + }, + { + "name": "Fall River Juvenile Court", + "tyler_lower_court_code": "1840", + "tyler_prod_lower_court_code": "7069", + "source_names": [ + "Fall River Juvenile Court" + ], + "cities": [ + "Fall River" + ] + }, + { + "name": "Falmouth District Court", + "tyler_lower_court_code": "1780", + "tyler_prod_lower_court_code": "7009", + "source_names": [ + "Falmouth District Court" + ], + "cities": [ + "Falmouth" + ] + }, + { + "name": "Falmouth Juvenile Court", + "tyler_lower_court_code": "1834", + "tyler_prod_lower_court_code": "7063", + "source_names": [ + "Falmouth Juvenile Court" + ], + "cities": [ + "Falmouth" + ] + }, + { + "name": "Fitchburg District Court", + "tyler_lower_court_code": "1781", + "tyler_prod_lower_court_code": "7010", + "source_names": [ + "Fitchburg District Court" + ], + "cities": [ + "Fitchburg" + ] + }, + { + "name": "Framingham District Court", + "tyler_lower_court_code": "1782", + "tyler_prod_lower_court_code": "7011", + "source_names": [ + "Framingham District Court" + ], + "cities": [ + "Framingham" + ] + }, + { + "name": "Franklin County Superior Court", + "tyler_lower_court_code": "1875", + "tyler_prod_lower_court_code": "7104", + "source_names": [ + "Franklin County Superior Court" + ], + "cities": [ + "Greenfield" + ] + }, + { + "name": "Franklin Probate and Family Court", + "tyler_lower_court_code": "1860", + "tyler_prod_lower_court_code": "7089", + "source_names": [ + "Franklin Probate and Family Court" + ], + "cities": [ + "Greenfield" + ] + }, + { + "name": "Gardner District Court", + "tyler_lower_court_code": "1783", + "tyler_prod_lower_court_code": "7012", + "source_names": [ + "Gardner District Court" + ], + "cities": [ + "Gardner" + ] + }, + { + "name": "Gloucester District Court", + "tyler_lower_court_code": "1784", + "tyler_prod_lower_court_code": "7013", + "source_names": [ + "Gloucester District Court" + ], + "cities": [ + "Gloucester" + ] + }, + { + "name": "Greenfield District Court", + "tyler_lower_court_code": "1785", + "tyler_prod_lower_court_code": "7014", + "source_names": [ + "Greenfield District Court" + ], + "cities": [ + "Greenfield" + ] + }, + { + "name": "Hampden County Superior Court", + "tyler_lower_court_code": "1869", + "tyler_prod_lower_court_code": "7098", + "source_names": [ + "Hampden County Superior Court" + ], + "cities": [ + "Springfield" + ] + }, + { + "name": "Hampden Probate and Family Court", + "tyler_lower_court_code": "1861", + "tyler_prod_lower_court_code": "7090", + "source_names": [ + "Hampden Probate and Family Court" + ], + "cities": [ + "Springfield" + ] + }, + { + "name": "Hampshire County Superior Court", + "tyler_lower_court_code": "1876", + "tyler_prod_lower_court_code": "7105", + "source_names": [ + "Hampshire County Superior Court" + ], + "cities": [ + "Northampton" + ] + }, + { + "name": "Hampshire Probate and Family Court", + "tyler_lower_court_code": "1862", + "tyler_prod_lower_court_code": "7091", + "source_names": [ + "Hampshire Probate and Family Court" + ], + "cities": [ + "Northampton" + ] + }, + { + "name": "Haverhill District Court", + "tyler_lower_court_code": "1786", + "tyler_prod_lower_court_code": "7015", + "source_names": [ + "Haverhill District Court" + ], + "cities": [ + "Haverhill" + ] + }, + { + "name": "Hingham District Court", + "tyler_lower_court_code": "1787", + "tyler_prod_lower_court_code": "7016", + "source_names": [ + "Hingham District Court" + ], + "cities": [ + "Hingham" + ] + }, + { + "name": "Hingham Juvenile Court", + "tyler_lower_court_code": "1850", + "tyler_prod_lower_court_code": "7079", + "source_names": [ + "Hingham Juvenile Court" + ], + "cities": [ + "Hingham" + ] + }, + { + "name": "Holyoke District Court", + "tyler_lower_court_code": "1788", + "tyler_prod_lower_court_code": "7017", + "source_names": [ + "Holyoke District Court" + ], + "cities": [ + "Holyoke" + ] + }, + { + "name": "Ipswich District Court", + "tyler_lower_court_code": "1789", + "tyler_prod_lower_court_code": "7018", + "source_names": [ + "Ipswich District Court" + ], + "cities": [ + "Newburyport" + ] + }, + { + "name": "Juvenile Court — Belchertown, Greenfield, Hadley, Orange", + "tyler_lower_court_code": "1845", + "tyler_prod_lower_court_code": "7074", + "source_names": [ + "Belchertown Juvenile Court", + "Greenfield Juvenile Court", + "Hadley Juvenile Court", + "Orange Juvenile Court" + ], + "cities": [ + "Belchertown", + "Greenfield", + "Hadley", + "Orange" + ] + }, + { + "name": "Juvenile Court — Boston, Chelsea, Dorchester, West Roxbury", + "tyler_lower_court_code": "1852", + "tyler_prod_lower_court_code": "7081", + "source_names": [ + "Boston Juvenile Court", + "Chelsea Juvenile Court", + "Dorchester Juvenile Court", + "West Roxbury Juvenile Court" + ], + "cities": [ + "Boston", + "Chelsea", + "Dorchester", + "Jamaica Plain" + ] + }, + { + "name": "Juvenile Court — Cambridge, Framingham, Lowell, Waltham", + "tyler_lower_court_code": "1847", + "tyler_prod_lower_court_code": "7076", + "source_names": [ + "Cambridge Juvenile Court", + "Framingham Juvenile Court", + "Lowell Juvenile Court", + "Waltham Juvenile Court" + ], + "cities": [ + "Cambridge", + "Framingham", + "Lowell", + "Waltham" + ] + }, + { + "name": "Juvenile Court — Dedham, Quincy, Stoughton", + "tyler_lower_court_code": "1848", + "tyler_prod_lower_court_code": "7077", + "source_names": [ + "Dedham Juvenile Court", + "Quincy Juvenile Court", + "Stoughton Juvenile Court" + ], + "cities": [ + "Dedham", + "Quincy", + "Stoughton" + ] + }, + { + "name": "Juvenile Court — Dudley, Fitchburg, Milford, Worcester", + "tyler_lower_court_code": "1853", + "tyler_prod_lower_court_code": "7082", + "source_names": [ + "Dudley Juvenile Court", + "Fitchburg Juvenile Court", + "Milford Juvenile Court", + "Worcester Juvenile Court" + ], + "cities": [ + "Dudley", + "Fitchburg", + "Milford", + "Worcester" + ] + }, + { + "name": "Juvenile Court — Great Barrington, North Adams, Pittsfield", + "tyler_lower_court_code": "1838", + "tyler_prod_lower_court_code": "7067", + "source_names": [ + "Great Barrington Juvenile Court", + "North Adams Juvenile Court", + "Pittsfield Juvenile Court" + ], + "cities": [ + "Great Barrington", + "North Adams", + "Pittsfield" + ] + }, + { + "name": "Juvenile Court — Holyoke, Palmer, Springfield", + "tyler_lower_court_code": "1846", + "tyler_prod_lower_court_code": "7075", + "source_names": [ + "Holyoke Juvenile Court", + "Palmer Juvenile Court", + "Springfield Juvenile Court" + ], + "cities": [ + "Holyoke", + "Palmer", + "Springfield" + ] + }, + { + "name": "Juvenile Court — Lawrence, Lynn, Newburyport, Salem", + "tyler_lower_court_code": "1844", + "tyler_prod_lower_court_code": "7073", + "source_names": [ + "Lawrence Juvenile Court", + "Lynn Juvenile Court", + "Newburyport Juvenile Court", + "Salem Juvenile Court" + ], + "cities": [ + "Lawrence", + "Lynn", + "Newburyport", + "Salem" + ] + }, + { + "name": "Land Court", + "tyler_lower_court_code": "1854", + "tyler_prod_lower_court_code": "7083", + "source_names": [ + "Land Court" + ], + "cities": [ + "Boston" + ] + }, + { + "name": "Lawrence District Court", + "tyler_lower_court_code": "1790", + "tyler_prod_lower_court_code": "7019", + "source_names": [ + "Lawrence District Court" + ], + "cities": [ + "Lawrence" + ] + }, + { + "name": "Leominster District Court", + "tyler_lower_court_code": "1791", + "tyler_prod_lower_court_code": "7020", + "source_names": [ + "Leominster District Court" + ], + "cities": [ + "Leominster" + ] + }, + { + "name": "Lowell District Court", + "tyler_lower_court_code": "1792", + "tyler_prod_lower_court_code": "7021", + "source_names": [ + "Lowell District Court" + ], + "cities": [ + "Lowell" + ] + }, + { + "name": "Lynn District Court", + "tyler_lower_court_code": "1793", + "tyler_prod_lower_court_code": "7022", + "source_names": [ + "Lynn District Court" + ], + "cities": [ + "Lynn" + ] + }, + { + "name": "Malden District Court", + "tyler_lower_court_code": "1794", + "tyler_prod_lower_court_code": "7023", + "source_names": [ + "Malden District Court" + ], + "cities": [ + "Medford" + ] + }, + { + "name": "Marlborough District Court", + "tyler_lower_court_code": "1795", + "tyler_prod_lower_court_code": "7024", + "source_names": [ + "Marlborough District Court" + ], + "cities": [ + "Marlborough" + ] + }, + { + "name": "Metro South / Southeast Housing Court — Barnstable, Brockton, Canton, Fall River, New Bedford, Plymouth, Taunton", + "tyler_lower_court_code": "1829", + "tyler_prod_lower_court_code": "7058", + "source_names": [ + "Metro South Housing Court - Brockton Session", + "Metro South Housing Court - Canton Session", + "Southeast Housing Court - Barnstable session", + "Southeast Housing Court - Fall River Session", + "Southeast Housing Court - New Bedford Session", + "Southeast Housing Court - Plymouth Session", + "Southeast Housing Court - Taunton Session" + ], + "cities": [ + "Barnstable", + "Brockton", + "Canton", + "Fall River", + "New Bedford", + "Plymouth", + "Taunton" + ] + }, + { + "name": "Middlesex County Superior Court (Lowell, Woburn)", + "tyler_lower_court_code": "1877", + "tyler_prod_lower_court_code": "7106", + "source_names": [ + "Middlesex County Superior Court" + ], + "cities": [ + "Lowell", + "Woburn" + ] + }, + { + "name": "Middlesex Probate and Family Court - North / Middlesex Probate and Family Court - South", + "tyler_lower_court_code": "1863", + "tyler_prod_lower_court_code": "7092", + "source_names": [ + "Middlesex Probate and Family Court - North", + "Middlesex Probate and Family Court - South" + ], + "cities": [ + "Lowell", + "Woburn" + ] + }, + { + "name": "Milford District Court", + "tyler_lower_court_code": "1796", + "tyler_prod_lower_court_code": "7025", + "source_names": [ + "Milford District Court" + ], + "cities": [ + "Milford" + ] + }, + { + "name": "Nantucket County Superior Court", + "tyler_lower_court_code": "1878", + "tyler_prod_lower_court_code": "7107", + "source_names": [ + "Nantucket County Superior Court" + ], + "cities": [ + "Nantucket" + ] + }, + { + "name": "Nantucket District Court", + "tyler_lower_court_code": "1797", + "tyler_prod_lower_court_code": "7026", + "source_names": [ + "Nantucket District Court" + ], + "cities": [ + "Nantucket" + ] + }, + { + "name": "Nantucket Juvenile Court", + "tyler_lower_court_code": "1835", + "tyler_prod_lower_court_code": "7064", + "source_names": [ + "Nantucket Juvenile Court" + ], + "cities": [ + "Nantucket" + ] + }, + { + "name": "Nantucket Probate and Family Court", + "tyler_lower_court_code": "1864", + "tyler_prod_lower_court_code": "7093", + "source_names": [ + "Nantucket Probate and Family Court" + ], + "cities": [ + "Nantucket" + ] + }, + { + "name": "Natick District Court", + "tyler_lower_court_code": "1798", + "tyler_prod_lower_court_code": "7027", + "source_names": [ + "Natick District Court" + ], + "cities": [ + "Framingham" + ] + }, + { + "name": "New Bedford District Court", + "tyler_lower_court_code": "1799", + "tyler_prod_lower_court_code": "7028", + "source_names": [ + "New Bedford District Court" + ], + "cities": [ + "New Bedford" + ] + }, + { + "name": "New Bedford Juvenile Court", + "tyler_lower_court_code": "1841", + "tyler_prod_lower_court_code": "7070", + "source_names": [ + "New Bedford Juvenile Court" + ], + "cities": [ + "New Bedford" + ] + }, + { + "name": "Newburyport District Court", + "tyler_lower_court_code": "1800", + "tyler_prod_lower_court_code": "7029", + "source_names": [ + "Newburyport District Court" + ], + "cities": [ + "Newburyport" + ] + }, + { + "name": "Newton District Court", + "tyler_lower_court_code": "1801", + "tyler_prod_lower_court_code": "7030", + "source_names": [ + "Newton District Court" + ], + "cities": [ + "West Newton" + ] + }, + { + "name": "Norfolk County Superior Court", + "tyler_lower_court_code": "1879", + "tyler_prod_lower_court_code": "7108", + "source_names": [ + "Norfolk County Superior Court" + ], + "cities": [ + "Dedham" + ] + }, + { + "name": "Norfolk Probate and Family Court", + "tyler_lower_court_code": "1865", + "tyler_prod_lower_court_code": "7094", + "source_names": [ + "Norfolk Probate and Family Court" + ], + "cities": [ + "Canton" + ] + }, + { + "name": "Northampton District Court", + "tyler_lower_court_code": "1802", + "tyler_prod_lower_court_code": "7031", + "source_names": [ + "Northampton District Court" + ], + "cities": [ + "Northampton" + ] + }, + { + "name": "Northeast Housing Court (Lawrence, Lowell, Lynn, Salem, Woburn sessions)", + "tyler_lower_court_code": "1828", + "tyler_prod_lower_court_code": "7057", + "source_names": [ + "Northeast Housing Court - Lawrence Session", + "Northeast Housing Court - Lowell Session", + "Northeast Housing Court - Lynn Session", + "Northeast Housing Court - Salem Session", + "Northeast Housing Court - Woburn Session" + ], + "cities": [ + "Lawrence", + "Lowell", + "St. Salem", + "Woburn" + ] + }, + { + "name": "Northern Berkshire District Court", + "tyler_lower_court_code": "1803", + "tyler_prod_lower_court_code": "7032", + "source_names": [ + "Northern Berkshire District Court" + ], + "cities": [ + "North Adams" + ] + }, + { + "name": "Orange District Court", + "tyler_lower_court_code": "1804", + "tyler_prod_lower_court_code": "7033", + "source_names": [ + "Orange District Court" + ], + "cities": [ + "Orange" + ] + }, + { + "name": "Orleans District Court", + "tyler_lower_court_code": "1805", + "tyler_prod_lower_court_code": "7034", + "source_names": [ + "Orleans District Court" + ], + "cities": [ + "Orleans" + ] + }, + { + "name": "Orleans Juvenile Court", + "tyler_lower_court_code": "1836", + "tyler_prod_lower_court_code": "7065", + "source_names": [ + "Orleans Juvenile Court" + ], + "cities": [ + "Orleans" + ] + }, + { + "name": "Palmer District Court", + "tyler_lower_court_code": "1806", + "tyler_prod_lower_court_code": "7035", + "source_names": [ + "Palmer District Court" + ], + "cities": [ + "Palmer" + ] + }, + { + "name": "Peabody District Court", + "tyler_lower_court_code": "1807", + "tyler_prod_lower_court_code": "7036", + "source_names": [ + "Peabody District Court" + ], + "cities": [ + "Peabody" + ] + }, + { + "name": "Pittsfield District Court", + "tyler_lower_court_code": "1808", + "tyler_prod_lower_court_code": "7037", + "source_names": [ + "Pittsfield District Court" + ], + "cities": [ + "Pittsfield" + ] + }, + { + "name": "Plymouth County Superior Court (Brockton)", + "tyler_lower_court_code": "1880", + "tyler_prod_lower_court_code": "7109", + "source_names": [ + "Plymouth County Superior Court" + ], + "cities": [ + "Brockton" + ] + }, + { + "name": "Plymouth County Superior Court (Plymouth)", + "tyler_lower_court_code": "1881", + "tyler_prod_lower_court_code": "7110", + "source_names": [ + "Plymouth County Superior Court" + ], + "cities": [ + "Plymouth" + ] + }, + { + "name": "Plymouth District Court", + "tyler_lower_court_code": "1809", + "tyler_prod_lower_court_code": "7038", + "source_names": [ + "Plymouth District Court" + ], + "cities": [ + "Plymouth" + ] + }, + { + "name": "Plymouth Juvenile Court", + "tyler_lower_court_code": "1837", + "tyler_prod_lower_court_code": "7066", + "source_names": [ + "Plymouth Juvenile Court" + ], + "cities": [ + "Plymouth" + ] + }, + { + "name": "Probate and Family Court — Bristol, Fall River, New Bedford", + "tyler_lower_court_code": "1857", + "tyler_prod_lower_court_code": "7086", + "source_names": [ + "Bristol Probate and Family Court", + "Fall River Probate and Family Court", + "New Bedford Probate and Family Court" + ], + "cities": [ + "Fall River", + "New Bedford", + "Taunton" + ] + }, + { + "name": "Probate and Family Court — Brockton, Plymouth", + "tyler_lower_court_code": "1866", + "tyler_prod_lower_court_code": "7095", + "source_names": [ + "Brockton Probate and Family Court", + "Plymouth Probate and Family Court" + ], + "cities": [ + "Brockton", + "Plymouth" + ] + }, + { + "name": "Probate and Family Court — Essex, Lawrence", + "tyler_lower_court_code": "1859", + "tyler_prod_lower_court_code": "7088", + "source_names": [ + "Essex Probate and Family Court", + "Lawrence Probate and Family Court" + ], + "cities": [ + "Lawrence", + "Salem" + ] + }, + { + "name": "Quincy District Court", + "tyler_lower_court_code": "1810", + "tyler_prod_lower_court_code": "7039", + "source_names": [ + "Quincy District Court" + ], + "cities": [ + "Quincy" + ] + }, + { + "name": "Roxbury Division, Boston Municipal Court", + "tyler_lower_court_code": "1758", + "tyler_prod_lower_court_code": "6987", + "source_names": [ + "Roxbury Division, Boston Municipal Court" + ], + "cities": [ + "Roxbury" + ] + }, + { + "name": "Salem District Court", + "tyler_lower_court_code": "1811", + "tyler_prod_lower_court_code": "7040", + "source_names": [ + "Salem District Court" + ], + "cities": [ + "Salem" + ] + }, + { + "name": "Somerville District Court", + "tyler_lower_court_code": "1812", + "tyler_prod_lower_court_code": "7041", + "source_names": [ + "Somerville District Court" + ], + "cities": [ + "Somerville" + ] + }, + { + "name": "South Boston Division, Boston Municipal Court", + "tyler_lower_court_code": "1759", + "tyler_prod_lower_court_code": "6988", + "source_names": [ + "South Boston Division, Boston Municipal Court" + ], + "cities": [ + "South Boston" + ] + }, + { + "name": "Southern Berkshire District Court", + "tyler_lower_court_code": "1813", + "tyler_prod_lower_court_code": "7042", + "source_names": [ + "Southern Berkshire District Court" + ], + "cities": [ + "Great Barrington" + ] + }, + { + "name": "Springfield District Court", + "tyler_lower_court_code": "1814", + "tyler_prod_lower_court_code": "7043", + "source_names": [ + "Springfield District Court" + ], + "cities": [ + "Springfield" + ] + }, + { + "name": "Stoughton District Court", + "tyler_lower_court_code": "1815", + "tyler_prod_lower_court_code": "7044", + "source_names": [ + "Stoughton District Court" + ], + "cities": [ + "Stoughton" + ] + }, + { + "name": "Suffolk County Superior Court", + "tyler_lower_court_code": "1882", + "tyler_prod_lower_court_code": "7111", + "source_names": [ + "Suffolk County Superior Court" + ], + "cities": [ + "Boston" + ] + }, + { + "name": "Suffolk Probate and Family Court", + "tyler_lower_court_code": "1867", + "tyler_prod_lower_court_code": "7096", + "source_names": [ + "Suffolk Probate and Family Court" + ], + "cities": [ + "Boston" + ] + }, + { + "name": "Taunton District Court", + "tyler_lower_court_code": "1816", + "tyler_prod_lower_court_code": "7045", + "source_names": [ + "Taunton District Court" + ], + "cities": [ + "Taunton" + ] + }, + { + "name": "Taunton Juvenile Court", + "tyler_lower_court_code": "1842", + "tyler_prod_lower_court_code": "7071", + "source_names": [ + "Taunton Juvenile Court" + ], + "cities": [ + "Taunton" + ] + }, + { + "name": "Uxbridge District Court", + "tyler_lower_court_code": "1817", + "tyler_prod_lower_court_code": "7046", + "source_names": [ + "Uxbridge District Court" + ], + "cities": [ + "Uxbridge" + ] + }, + { + "name": "Waltham District Court", + "tyler_lower_court_code": "1818", + "tyler_prod_lower_court_code": "7047", + "source_names": [ + "Waltham District Court" + ], + "cities": [ + "Waltham" + ] + }, + { + "name": "Wareham District Court", + "tyler_lower_court_code": "1819", + "tyler_prod_lower_court_code": "7048", + "source_names": [ + "Wareham District Court" + ], + "cities": [ + "Wareham" + ] + }, + { + "name": "Wareham Juvenile Court", + "tyler_lower_court_code": "1851", + "tyler_prod_lower_court_code": "7080", + "source_names": [ + "Wareham Juvenile Court" + ], + "cities": [ + "West Wareham" + ] + }, + { + "name": "West Roxbury Division, Boston Municipal Court", + "tyler_lower_court_code": "1760", + "tyler_prod_lower_court_code": "6989", + "source_names": [ + "West Roxbury Division, Boston Municipal Court" + ], + "cities": [ + "Jamaica Plain" + ] + }, + { + "name": "Westborough District Court", + "tyler_lower_court_code": "1820", + "tyler_prod_lower_court_code": "7049", + "source_names": [ + "Westborough District Court" + ], + "cities": [ + "Westborough" + ] + }, + { + "name": "Western Housing Court (Greenfield, Hadley, Pittsfield, Springfield sessions)", + "tyler_lower_court_code": "1830", + "tyler_prod_lower_court_code": "7059", + "source_names": [ + "Western Housing Court - Greenfield Session", + "Western Housing Court - Hadley Session", + "Western Housing Court - Pittsfield Session", + "Western Housing Court - Springfield Session" + ], + "cities": [ + "Greenfield", + "Hadley", + "Pittsfield", + "Springfield" + ] + }, + { + "name": "Westfield District Court", + "tyler_lower_court_code": "1821", + "tyler_prod_lower_court_code": "7050", + "source_names": [ + "Westfield District Court" + ], + "cities": [ + "Westfield" + ] + }, + { + "name": "Winchendon District Court", + "tyler_lower_court_code": "1822", + "tyler_prod_lower_court_code": "7051", + "source_names": [ + "Winchendon District Court" + ], + "cities": [ + "Gardner District Court, Gardner" + ] + }, + { + "name": "Woburn District Court", + "tyler_lower_court_code": "1823", + "tyler_prod_lower_court_code": "7052", + "source_names": [ + "Woburn District Court" + ], + "cities": [ + "Woburn" + ] + }, + { + "name": "Worcester County Superior Court", + "tyler_lower_court_code": "1883", + "tyler_prod_lower_court_code": "7112", + "source_names": [ + "Worcester County Superior Court" + ], + "cities": [ + "Worcester" + ] + }, + { + "name": "Worcester District Court", + "tyler_lower_court_code": "1824", + "tyler_prod_lower_court_code": "7053", + "source_names": [ + "Worcester District Court" + ], + "cities": [ + "Worcester" + ] + }, + { + "name": "Worcester Probate and Family Court", + "tyler_lower_court_code": "1868", + "tyler_prod_lower_court_code": "7097", + "source_names": [ + "Worcester Probate and Family Court" + ], + "cities": [ + "Worcester" + ] + }, + { + "name": "Wrentham District Court", + "tyler_lower_court_code": "1825", + "tyler_prod_lower_court_code": "7054", + "source_names": [ + "Wrentham District Court" + ], + "cities": [ + "Wrentham" + ] + } + ] +} diff --git a/efile_app/efile/management/commands/seed_accessibility_session.py b/efile_app/efile/management/commands/seed_accessibility_session.py index 8f3b65f8..01bc5dee 100644 --- a/efile_app/efile/management/commands/seed_accessibility_session.py +++ b/efile_app/efile/management/commands/seed_accessibility_session.py @@ -20,18 +20,35 @@ class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument("--output", required=True, help="Path for Playwright storage state JSON.") parser.add_argument("--origin", default="http://127.0.0.1:8000", help="Origin served to Playwright.") + parser.add_argument( + "--appeal", + action="store_true", + help="Seed a new-appeal draft for appellate browser checks.", + ) + parser.add_argument( + "--jurisdiction", + choices=("illinois", "massachusetts"), + default="illinois", + help="Jurisdiction for the browser fixture (default: illinois).", + ) def handle(self, *args, **options): user_model = settings.AUTH_USER_MODEL from django.apps import apps user_class = apps.get_model(user_model) + jurisdiction = options["jurisdiction"] + is_illinois = jurisdiction == "illinois" + username = "accessibility-checker" if is_illinois else f"accessibility-checker-{jurisdiction}" + email = ( + "accessibility-checker@example.com" if is_illinois else f"accessibility-checker-{jurisdiction}@example.com" + ) user, _ = user_class.objects.update_or_create( - username="accessibility-checker", + username=username, defaults={ - "email": "accessibility-checker@example.com", - "tyler_jurisdiction": "illinois", - "tyler_username": "accessibility-checker@example.com", + "email": email, + "tyler_jurisdiction": jurisdiction, + "tyler_username": email, "first_name": "Avery", "last_name": "Checker", }, @@ -41,22 +58,39 @@ def handle(self, *args, **options): FilingDraft.objects.filter(user=user).delete() FilingPlan.objects.filter(user=user).delete() - plan = FilingPlan.objects.create(user=user, jurisdiction="illinois", title="Accessibility test filing") + plan = FilingPlan.objects.create(user=user, jurisdiction=jurisdiction, title="Accessibility test filing") + appeal = options["appeal"] + if appeal and jurisdiction not in {"illinois", "massachusetts"}: + raise ValueError("The appeal browser fixture supports Illinois and Massachusetts.") + court_code = ("TAC1" if is_illinois else "appeals:acp") if appeal else "cook:cvd1" + court_name = ( + ("Appellate Court - 1st District" if is_illinois else "Massachusetts Appeals Court (Panel)") + if appeal + else "Cook County" + ) + case_category_code = ("21083" if is_illinois else "5796") if appeal else "6198" + case_category_name = ( + ("Appeal" if is_illinois else "Appeals Court Panel Cases - Civil") if appeal else "Small Claims" + ) + case_type_code = ("37653" if is_illinois else "7660") if appeal else "183541" + case_type_name = ("Notice of Appeal - Civil" if is_illinois else "Contract dispute") if appeal else "Contract" + filing_type_code = "30341" if appeal else "143132" + filing_type_name = "Notice of Appeal" if appeal else "Complaint" draft = FilingDraft.objects.create( user=user, plan=plan, - jurisdiction="illinois", + jurisdiction=jurisdiction, workflow_version=2, existing_case=ExistingCase.NEW, - current_step=WorkflowStepKey.REVIEW, - court_code="cook:cvd1", - court_name="Cook County", - case_category_code="6198", - case_category_name="Small Claims", - case_type_code="183541", - case_type_name="Contract", - filing_type_code="143132", - filing_type_name="Complaint", + current_step=WorkflowStepKey.PARTIES if appeal else WorkflowStepKey.REVIEW, + court_code=court_code, + court_name=court_name, + case_category_code=case_category_code, + case_category_name=case_category_name, + case_type_code=case_type_code, + case_type_name=case_type_name, + filing_type_code=filing_type_code, + filing_type_name=filing_type_name, document_checklist_acknowledged=True, # Something for the confirm-filing screen to show, so its # "I checked this" acknowledgement is rendered and audited too. @@ -67,13 +101,13 @@ def handle(self, *args, **options): FilingDocument.objects.create( draft=draft, role=FilingDocument.Role.LEAD, - name="Accessibility complaint.pdf", - original_filename="Accessibility complaint.pdf", + name=f"{filing_type_name}.pdf" if appeal else "Accessibility complaint.pdf", + original_filename=f"{filing_type_name}.pdf" if appeal else "Accessibility complaint.pdf", # This fixture starts downstream of preparation and confirmation. preparation="unchanged", preparation_reviewed_at=timezone.now(), - filing_type_code="143132", - filing_type_name="Complaint", + filing_type_code=filing_type_code, + filing_type_name=filing_type_name, document_type_code="public", document_type_name="Public", ) @@ -84,9 +118,9 @@ def handle(self, *args, **options): first_name="Avery", last_name="Checker", address_line_1="100 Main Street", - city="Chicago", - state="IL", - zip_code="60601", + city="Chicago" if is_illinois else "Boston", + state="IL" if is_illinois else "MA", + zip_code="60601" if is_illinois else "02108", email=user.email, party_type="plaintiff", party_type_name="Plaintiff", @@ -98,6 +132,8 @@ def handle(self, *args, **options): sort_order=1, first_name="Jordan", last_name="Example", + party_type="defendant" if appeal else "", + party_type_name="Defendant" if appeal else "", ) # A quote priced on this draft as it now stands, so Review shows it as @@ -111,8 +147,8 @@ def handle(self, *args, **options): client.force_login(user) session = client.session session[CURRENT_DRAFT_SESSION_KEY] = draft.pk - session["jurisdiction"] = "illinois" - session["auth_tokens"] = {"TYLER-TOKEN-ILLINOIS": "accessibility-test-token"} + session["jurisdiction"] = jurisdiction + session["auth_tokens"] = {f"TYLER-TOKEN-{jurisdiction.upper()}": "accessibility-test-token"} session.save() output = Path(options["output"]) diff --git a/efile_app/efile/services/appeals.py b/efile_app/efile/services/appeals.py new file mode 100644 index 00000000..a24564f4 --- /dev/null +++ b/efile_app/efile/services/appeals.py @@ -0,0 +1,130 @@ +"""Originating case details for new appellate cases. + +Massachusetts uses unpublished, environment-specific lower-court IDs maintained +by MACourts. Other jurisdictions use the EFSP's court location codes. +""" + +import hashlib +import json +import logging +from pathlib import Path + +import requests +from django.conf import settings +from django.core.cache import cache + +from efile.services.court_selection import is_non_filing_court + +logger = logging.getLogger(__name__) +LOWER_COURT_FIELDS = ( + "lower_court_code", + "lower_court_name", + "lower_court_prod_code", + "lower_court_docket_number", + "lower_court_title", + "lower_court_judge", +) + + +def code_list(jurisdiction, path, lookups=None): + """Return an EFSP code list, or None when it could not be loaded. + + Pass the payload's ``lookups`` to fetch a cache miss within that request's + time budget. Its failures are not remembered: a spent budget belongs to one + request, not to the EFSP. + """ + url = f"{settings.EFSP_URL}/jurisdictions/{jurisdiction}/codes/{path}" + key = "appeal-codes:" + hashlib.sha256(url.encode()).hexdigest() + result = cache.get(key) + if result is not None: + return result if isinstance(result, list) else None + if lookups is not None: + result = lookups.get(url) + if not isinstance(result, list): + return None + cache.set(key, result, 300) + return result + try: + response = requests.get(url, timeout=10) + response.raise_for_status() + result = response.json() + if isinstance(result, list): + cache.set(key, result, 300) + return result + except (requests.RequestException, ValueError): + logger.warning("Could not load appellate code list %s", url) + # A single page consults this list for routing, validation, and its summary. + # Briefly remember an outage so those checks do not each wait ten seconds. + cache.set(key, False, 30) + return None + + +def is_new_appeal(draft): + if draft.existing_case != "new" or draft.previous_case_id or not draft.court_code or not draft.case_category_code: + return False + categories = code_list(draft.jurisdiction, f"courts/{draft.court_code}/categories") + for category in categories or []: + if isinstance(category, dict) and str(category.get("code")) == str(draft.case_category_code): + return category.get("ecfcasetype") == "AppellateCase" + # Keep the questions reachable during an outage, including for saved drafts. + description = f"{draft.court_name} {draft.case_category_name}".lower() + return any(word in description for word in ("appeal", "appellate", "supreme", "single justice")) + + +def lower_court_options(jurisdiction, lookups=None): + """Return the lower-court choices, or None when the EFSP's list is unavailable.""" + if jurisdiction == "massachusetts": + source = Path(__file__).resolve().parent.parent / "data" / "massachusetts_lower_courts.json" + rows = json.loads(source.read_text())["courts"] + return [ + { + "value": row["tyler_lower_court_code"], + "label": row["name"], + "prod_code": row["tyler_prod_lower_court_code"], + } + for row in rows + ] + rows = code_list(jurisdiction, "courts/?fileable_only=false&with_names=true", lookups=lookups) + if rows is None: + return None + return sorted( + [ + {"value": str(row["code"]), "label": row["name"], "prod_code": str(row["code"])} + for row in rows + if isinstance(row, dict) + and row.get("code") + and row.get("name") + and not is_non_filing_court(row["name"]) + and not any(word in row["name"].lower() for word in ("appellate", "appeals court", "supreme")) + ], + key=lambda row: row["label"], + ) + + +def appeal_questions(draft): + if not is_new_appeal(draft): + return [] + return [ + { + "name": "lower_court_code", + "label": "Lower court", + "type": "select", + "required": True, + "options": lower_court_options(draft.jurisdiction) or [], + }, + {"name": "lower_court_docket_number", "label": "Lower court case number", "type": "text", "required": True}, + {"name": "lower_court_title", "label": "Lower court case caption", "type": "text", "required": True}, + {"name": "lower_court_judge", "label": "Lower court judge", "type": "text", "required": False}, + ] + + +def appeal_answers_complete(draft): + questions = appeal_questions(draft) + answers = draft.supplemental_fields or {} + for question in questions: + value = str(answers.get(question["name"]) or "").strip() + if question["required"] and not value: + return False + if question["type"] == "select" and value not in {o["value"] for o in question["options"]}: + return False + return True diff --git a/efile_app/efile/services/drafts.py b/efile_app/efile/services/drafts.py index 3ca46035..da651776 100644 --- a/efile_app/efile/services/drafts.py +++ b/efile_app/efile/services/drafts.py @@ -353,6 +353,11 @@ def read_case_data(draft: FilingDraft | None) -> dict[str, Any]: for key, value in (draft.supplemental_fields or {}).items(): data[key] = value + from efile.services.appeals import LOWER_COURT_FIELDS, is_new_appeal + + if any(key in data for key in LOWER_COURT_FIELDS) and not is_new_appeal(draft): + for key in LOWER_COURT_FIELDS: + data.pop(key, None) return data diff --git a/efile_app/efile/services/efsp_payload.py b/efile_app/efile/services/efsp_payload.py index 62effad1..8b86c181 100644 --- a/efile_app/efile/services/efsp_payload.py +++ b/efile_app/efile/services/efsp_payload.py @@ -126,6 +126,7 @@ def prepare_efile_payload(efile_data, jurisdiction_id, court_id): """ lookups = _EfspLookups() _clean_case_identifiers(efile_data) + validate_lower_court(efile_data, jurisdiction_id, court_id, lookups=lookups) _drop_empty_cross_references(efile_data) substitute_test_document_urls(efile_data) validate_document_selections(efile_data) @@ -136,8 +137,9 @@ def prepare_efile_payload(efile_data, jurisdiction_id, court_id): def _clean_case_identifiers(efile_data): - """Ensure docket_number is omitted for new cases where there is no previous_case_id.""" - if not efile_data.get("previous_case_id") or efile_data.get("user_started_case") is True: + """New cases have neither a previous case ID nor a court-assigned docket number.""" + if not str(efile_data.get("previous_case_id") or "").strip() or efile_data.get("user_started_case") is True: + efile_data.pop("previous_case_id", None) efile_data.pop("docket_number", None) @@ -430,3 +432,54 @@ def _lookup_optional_services_meta(jurisdiction_id, court_id, filing_type, looku if not isinstance(services, list): return None return {service["code"]: service for service in parse_optional_services(services)} + + +def validate_lower_court(efile_data, jurisdiction_id, court_id, lookups=None): + """Use the same court options for fee quotes and submission as the form. + + When the EFSP's court list is unavailable, the filer's choice is passed + through rather than rejected; the EFSP stays the authority. + """ + from efile.services.appeals import code_list, lower_court_options + + lookups = lookups or _EfspLookups() + + if efile_data.get("previous_case_id"): + efile_data.pop("lower_court_case", None) + efile_data.pop("trial_court", None) + return + category_code = efile_data.get("efile_case_category") + categories = code_list(jurisdiction_id, f"courts/{court_id}/categories", lookups=lookups) if category_code else [] + category = next( + (row for row in categories or [] if isinstance(row, dict) and str(row.get("code")) == str(category_code)), None + ) + if category is not None and category.get("ecfcasetype") != "AppellateCase": + efile_data.pop("lower_court_case", None) + efile_data.pop("trial_court", None) + return + lower = efile_data.get("lower_court_case") + if category is None and not lower: + return + if not isinstance(lower, dict) or not all(str(lower.get(key) or "").strip() for key in ("title", "docket_number")): + raise PayloadValidationError( + "Enter the lower court case number and caption before checking fees or submitting." + ) + court = efile_data.get("trial_court") or {} + code = court.get("tyler_lower_court_code") if isinstance(court, dict) else None + options = lower_court_options(jurisdiction_id, lookups=lookups) + if options is None and code: + logger.warning("Lower courts for %s are unavailable; passing %s through unchecked", jurisdiction_id, code) + # Only Massachusetts has separate production codes, and its list is local. + selected = {"value": code, "label": court.get("name") or code, "prod_code": code} + else: + selected = next((option for option in options or [] if option["value"] == code), None) + if selected is None: + raise PayloadValidationError("Choose a valid lower court from the list before checking fees or submitting.") + efile_data["trial_court"] = { + "name": selected["label"], + "tyler_lower_court_code": selected["value"], + "tyler_prod_lower_court_code": selected["prod_code"], + } + efile_data["lower_court_case"] = { + key: str(lower.get(key) or "").strip() for key in ("title", "docket_number", "judge") + } diff --git a/efile_app/efile/services/fee_quotes.py b/efile_app/efile/services/fee_quotes.py index a57942fc..f5c620a7 100644 --- a/efile_app/efile/services/fee_quotes.py +++ b/efile_app/efile/services/fee_quotes.py @@ -65,6 +65,9 @@ def fee_inputs(draft: FilingDraft, *, payment_account_id: str | None = None) -> "existing_case": draft.existing_case, "previous_case_id": draft.previous_case_id, "amount_in_controversy": draft.amount_in_controversy, + "lower_court": { + key: value for key, value in (draft.supplemental_fields or {}).items() if key.startswith("lower_court_") + }, "optional_services": json.dumps(draft.optional_services or [], sort_keys=True, default=str), "payment_account": draft.selected_payment_account_id if payment_account_id is None else payment_account_id, "documents": documents, diff --git a/efile_app/efile/services/people.py b/efile_app/efile/services/people.py index 7cc63646..4900f203 100644 --- a/efile_app/efile/services/people.py +++ b/efile_app/efile/services/people.py @@ -9,6 +9,7 @@ from efile.models import FilingDocument, FilingDraft, FilingParty from efile.party_sides import PARTY_SIDE_KEYWORDS, PartySide, side_for_party_type_name +from efile.services.appeals import appeal_questions from efile.services.document_checklists import party_type_keywords_for_role from efile.services.extracted_parties import extracted_party_suggestions, party_display_name from efile.services.party_requirements import address_is_blank, address_is_complete, party_address_requirement @@ -548,7 +549,7 @@ def get_case_questions(draft: FilingDraft) -> list[dict[str, Any]]: case_type, court=draft.court_code, ) - questions: list[dict[str, Any]] = [] + questions: list[dict[str, Any]] = appeal_questions(draft) for section in (config or {}).get("sections", {}).values(): for group in section.get("fields", []): for field in group.get("fields", []): @@ -571,6 +572,8 @@ def get_case_questions(draft: FilingDraft) -> list[dict[str, Any]]: def parse_question_answer(question: dict[str, Any], value: Any) -> Any: + if question["type"] == "select" and value not in {option["value"] for option in question["options"]}: + raise ValueError("Choose a court from the list.") if question["type"] == "radio": if str(value).lower() == "true": return True diff --git a/efile_app/efile/static/js/filing-payload.js b/efile_app/efile/static/js/filing-payload.js index 8e0007b5..aef24a36 100644 --- a/efile_app/efile/static/js/filing-payload.js +++ b/efile_app/efile/static/js/filing-payload.js @@ -301,12 +301,29 @@ const FilingPayload = { return_date: "" }; + this.addLowerCourt(efilingData, caseData); + // Add court bundles for documents this.addCourtBundles(efilingData, uploadData, caseData, users); return efilingData; }, + addLowerCourt(efilingData, caseData) { + if (!caseData.previous_case_id && caseData.existing_case !== "existing" && caseData.lower_court_code) { + efilingData.lower_court_case = { + title: caseData.lower_court_title || "", + docket_number: caseData.lower_court_docket_number || "", + judge: caseData.lower_court_judge || "" + }; + efilingData.trial_court = { + name: caseData.lower_court_name, + tyler_lower_court_code: caseData.lower_court_code, + tyler_prod_lower_court_code: caseData.lower_court_prod_code + }; + } + }, + addCourtBundles(efilingData, uploadData, caseData, users) { const courtName = caseData.court_name || caseData.court || ""; // Cook and DuPage require a local attorney/SRL identifier. The code-list diff --git a/efile_app/efile/templates/efile/case_questions.html b/efile_app/efile/templates/efile/case_questions.html index 18d00322..fe1bee5f 100644 --- a/efile_app/efile/templates/efile/case_questions.html +++ b/efile_app/efile/templates/efile/case_questions.html @@ -35,6 +35,21 @@
{% translate "We could not load the lower courts. Reload this page to try again." %}
+ {% endif %} {% elif question.type == "number" %} 100 + assert len({option["value"] for option in options}) == len(options) + brighton = next(row for row in options if row["value"] == "1753") + assert brighton["prod_code"] == "6982" + assert "Brighton" in brighton["label"] + + +@pytest.mark.parametrize( + "jurisdiction,code,prod", [("illinois", "cook:law1", "cook:law1"), ("massachusetts", "1753", "6982")] +) +def test_payload_formats_lower_court(catalog, jurisdiction, code, prod): + payload = { + "efile_case_category": "appeal", + "previous_case_id": "", + "docket_number": "not-an-appellate-case-number", + "lower_court_case": {"title": " Example v. Test ", "docket_number": "24-CV-001"}, + "trial_court": {"tyler_lower_court_code": code, "tyler_prod_lower_court_code": "wrong"}, + } + _clean_case_identifiers(payload) + validate_lower_court(payload, jurisdiction, "court") + assert "previous_case_id" not in payload + assert "docket_number" not in payload + assert payload["trial_court"]["tyler_prod_lower_court_code"] == prod + assert payload["lower_court_case"] == {"title": "Example v. Test", "docket_number": "24-CV-001", "judge": ""} + + +def test_payload_rejects_missing_or_invalid_lower_court(catalog): + with pytest.raises(PayloadValidationError): + validate_lower_court({"efile_case_category": "appeal"}, "illinois", "TAC1") + with pytest.raises(PayloadValidationError): + validate_lower_court( + { + "efile_case_category": "appeal", + "lower_court_case": {"title": "Caption", "docket_number": "24-1"}, + "trial_court": {"tyler_lower_court_code": "invented"}, + }, + "illinois", + "TAC1", + ) + + +class FakeLookups: + def __init__(self, response): + self.response = response + self.urls = [] + + def get(self, url): + self.urls.append(url) + return self.response + + +@pytest.fixture +def live_code_lists(monkeypatch): + """Undo the autouse stub so lookups reach the given ``FakeLookups``.""" + monkeypatch.setattr(appeals, "code_list", live_code_list) + cache.clear() + yield + cache.clear() + + +def test_payload_passes_lower_court_through_when_efsp_list_is_unavailable(live_code_lists): + payload = { + "efile_case_category": "appeal", + "lower_court_case": {"title": "Caption", "docket_number": "24-1"}, + "trial_court": {"tyler_lower_court_code": "cook:law1", "name": "Cook County Law Division"}, + } + validate_lower_court(payload, "illinois", "TAC1", lookups=FakeLookups(None)) + assert payload["trial_court"] == { + "name": "Cook County Law Division", + "tyler_lower_court_code": "cook:law1", + "tyler_prod_lower_court_code": "cook:law1", + } + with pytest.raises(PayloadValidationError): + validate_lower_court( + {"efile_case_category": "appeal", "lower_court_case": {"title": "Caption", "docket_number": "24-1"}}, + "illinois", + "TAC1", + lookups=FakeLookups(None), + ) + + +def test_payload_lookups_use_the_request_budget_and_do_not_cache_its_failures(live_code_lists, monkeypatch): + def unbudgeted(*args, **kwargs): + raise AssertionError("lower-court lookups must go through the payload's lookups") + + monkeypatch.setattr(appeals.requests, "get", unbudgeted) + lookups = FakeLookups(None) + payload = { + "efile_case_category": "appeal", + "lower_court_case": {"title": "Caption", "docket_number": "24-1"}, + "trial_court": {"tyler_lower_court_code": "cook:law1"}, + } + validate_lower_court(payload, "illinois", "TAC1", lookups=lookups) + assert [url.rpartition("/codes/")[2] for url in lookups.urls] == [ + "courts/TAC1/categories", + "courts/?fileable_only=false&with_names=true", + ] + courts = [{"code": "cook:law1", "name": "Cook County Law Division"}] + assert appeals.code_list("illinois", "courts/TAC1/categories", lookups=FakeLookups(courts)) == courts + + +@pytest.mark.parametrize("extra", [{"previous_case_id": "123"}, {"efile_case_category": "civil"}]) +def test_stale_lower_court_is_omitted(catalog, extra): + payload = {"efile_case_category": "appeal", "lower_court_case": {"title": "old"}, "trial_court": {}} | extra + validate_lower_court(payload, "illinois", "TAC1") + assert "lower_court_case" not in payload + assert "trial_court" not in payload + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "jurisdiction,code,name", + [("illinois", "cook:law1", "Cook County Law Division"), ("massachusetts", "1753", "Brighton Division")], +) +def test_questions_validate_persist_review_and_edit(client, django_user_model, catalog, jurisdiction, code, name): + user = django_user_model.objects.create_user(username=f"appeal-{jurisdiction}", tyler_jurisdiction=jurisdiction) + draft = FilingDraft.objects.create( + user=user, + jurisdiction=jurisdiction, + workflow_version=2, + existing_case="new", + court_code="appellate", + case_category_code="appeal", + case_type_code="appeal-type", + selected_payment_account_id="waiver", + selected_payment_account_type="WV", + ) + reviewed_document(draft=draft, role="lead") + client.force_login(user) + session = client.session + session[CURRENT_DRAFT_SESSION_KEY] = draft.pk + session["auth_tokens"] = {f"TYLER-TOKEN-{jurisdiction.upper()}": "test-token"} + session["jurisdiction"] = jurisdiction + session.save() + url = reverse("case_questions", kwargs={"jurisdiction": jurisdiction}) + review = reverse("case_review", kwargs={"jurisdiction": jurisdiction}) + assert client.get(review).url.partition("?")[0] == url + response = client.get(url) + assert b'name="lower_court_code"' in response.content + assert name.encode() in response.content + data = { + "lower_court_code": "invalid", + "lower_court_title": "Example v. Test", + "lower_court_docket_number": "24-CV-001", + "lower_court_judge": "Judge Example", + "return_to": "review", + } + response = client.post(url, data) + assert response.status_code == 200 + assert b"Answer these questions" in response.content + assert b"Example v. Test" in response.content + assert b'name="return_to" value="review"' in response.content + data["lower_court_code"] = code + assert client.post(url, data).status_code == 302 + draft.refresh_from_db() + assert read_case_data(draft)["lower_court_code"] == code + before = fee_fingerprint(draft) + response = client.get(review) + assert response.status_code == 200 + assert name.encode() in response.content + assert b"24-CV-001" in response.content + assert b"Example v. Test" in response.content + assert b"Judge Example" in response.content + data["lower_court_docket_number"] = "24-CV-002" + client.post(url, data) + draft.refresh_from_db() + assert fee_fingerprint(draft) != before + assert b"24-CV-002" in client.get(review).content + draft.existing_case = "existing" + draft.previous_case_id = "existing-case" + draft.save() + assert "lower_court_code" not in read_case_data(draft) + assert client.get(url).status_code == 302 diff --git a/efile_app/efile/views/case_questions.py b/efile_app/efile/views/case_questions.py index 583483b8..029f9012 100644 --- a/efile_app/efile/views/case_questions.py +++ b/efile_app/efile/views/case_questions.py @@ -5,6 +5,7 @@ from django.views.decorators.http import require_http_methods from efile.api.suffolk_api_views import get_tyler_token +from efile.services.appeals import LOWER_COURT_FIELDS from efile.services.current_drafts import ensure_current_draft from efile.services.drafts import draft_snapshot from efile.services.people import get_case_questions, needs_amount_in_controversy, parse_question_answer @@ -41,7 +42,11 @@ def case_questions(request, jurisdiction): return redirect(get_step_url(next_step, jurisdiction)) for question in questions: - raw_value = (draft.supplemental_fields or {}).get(question["name"], "") + raw_value = ( + request.POST.get(question["name"], "") + if request.method == "POST" + else (draft.supplemental_fields or {}).get(question["name"], "") + ) if question["type"] == "radio": raw_value = "" if raw_value in (None, "") else str(raw_value).lower() elif raw_value is None: @@ -52,7 +57,7 @@ def case_questions(request, jurisdiction): answers = {} errors = [] for question in questions: - value = request.POST.get(question["name"], "") + value = request.POST.get(question["name"], "").strip() if question["name"] == "child_count" and request.POST.get("has_children") == "false": value = "" if question["required"] and value == "" and question["name"] != "child_count": @@ -74,6 +79,15 @@ def case_questions(request, jurisdiction): if errors: messages.error(request, f"Answer these questions: {', '.join(dict.fromkeys(errors))}.") else: + lower_question = next((q for q in questions if q["name"] == "lower_court_code"), None) + if lower_question: + selected = next(o for o in lower_question["options"] if o["value"] == answers["lower_court_code"]) + answers["lower_court_name"] = selected["label"] + answers["lower_court_prod_code"] = selected["prod_code"] + else: + draft.supplemental_fields = { + k: v for k, v in (draft.supplemental_fields or {}).items() if k not in LOWER_COURT_FIELDS + } draft.supplemental_fields = { **(draft.supplemental_fields or {}), **answers, @@ -95,7 +109,7 @@ def case_questions(request, jurisdiction): "filing_draft": draft_snapshot(draft), "questions": questions, "answers": draft.supplemental_fields or {}, - "return_to": request.GET.get("return_to", ""), + "return_to": request.POST.get("return_to") or request.GET.get("return_to", ""), "show_amount_field": show_amount_field, "amount_in_controversy": draft.amount_in_controversy, } diff --git a/efile_app/efile/views/payment.py b/efile_app/efile/views/payment.py index 85da4c36..f95106c4 100644 --- a/efile_app/efile/views/payment.py +++ b/efile_app/efile/views/payment.py @@ -7,6 +7,7 @@ from efile.api.suffolk_api_views import get_tyler_token from efile.models import FilingDocument, FilingParty +from efile.services.appeals import appeal_answers_complete from efile.services.current_drafts import ensure_current_draft from efile.services.draft_urls import draft_url from efile.services.drafts import draft_snapshot, read_case_data @@ -46,6 +47,10 @@ def efile_payment(request, jurisdiction): messages.error(request, "Complete the people in this filing before choosing payment.") return redirect("parties", jurisdiction=jurisdiction) + if not appeal_answers_complete(draft): + messages.error(request, "Complete the lower court information before checking fees.") + return redirect("case_questions", jurisdiction=jurisdiction) + if request.method == "POST": account_id = request.POST.get("selected_payment_account", "").strip() try: diff --git a/efile_app/efile/views/review.py b/efile_app/efile/views/review.py index 6095836c..030a1d2e 100644 --- a/efile_app/efile/views/review.py +++ b/efile_app/efile/views/review.py @@ -3,6 +3,7 @@ from efile.api.suffolk_api_views import get_tyler_token from efile.models import FilingDocument, FilingParty +from efile.services.appeals import LOWER_COURT_FIELDS, appeal_answers_complete from efile.services.current_drafts import ensure_current_draft from efile.services.disclaimers import disclaimer_context from efile.services.document_previews import unreviewed_documents @@ -52,6 +53,10 @@ def case_review(request, jurisdiction): if unreviewed_documents(draft).exists(): return redirect(get_step_url(WorkflowStepKey.PREVIEW_DOCUMENTS, jurisdiction) + "?return_to=review") + if not appeal_answers_complete(draft): + messages.error(request, "Complete the lower court information before reviewing your filing.") + return redirect(get_step_url(WorkflowStepKey.CASE_QUESTIONS, jurisdiction) + "?return_to=review") + if not draft.selected_payment_account_id: messages.error(request, "Choose a payment method before reviewing your filing.") return redirect("payment", jurisdiction=jurisdiction) @@ -60,10 +65,18 @@ def case_review(request, jurisdiction): question_answers = [ { "label": question_labels.get(key, key.replace("_", " ").title()), - "value": "Yes" if value is True else "No" if value is False else value, + "value": ((draft.supplemental_fields or {}).get("lower_court_name") or value) + if key == "lower_court_code" + else "Yes" + if value is True + else "No" + if value is False + else value, } for key, value in (draft.supplemental_fields or {}).items() - if not key.startswith("_") and value not in (None, "") + if not key.startswith("_") + and value not in (None, "") + and (key not in LOWER_COURT_FIELDS or key in question_labels) ] parties = list(FilingParty.objects.filter(draft=draft).order_by("sort_order", "created_at")) filer = next((party for party in parties if party.role == "filer"), None) diff --git a/efile_app/js-tests/filing-payload.test.js b/efile_app/js-tests/filing-payload.test.js index de9cbb45..5eaa6a07 100644 --- a/efile_app/js-tests/filing-payload.test.js +++ b/efile_app/js-tests/filing-payload.test.js @@ -629,4 +629,43 @@ test("identical PDF names and URLs remain separate filings with separate codes", assert.equal(bundles.length, 2); assert.deepStrictEqual(bundles.map(bundle => bundle.filename), ["appearance.pdf", "appearance.pdf"]); assert.deepStrictEqual(bundles.map(bundle => bundle.filing_type), ["appearance", "jury-demand"]); +}); +test("new appeal payload keeps the originating case separate from the appellate case", () => { + const handler = makeHandler(); + const caseData = { + existing_case: "new", + previous_case_id: "", + filing_parties: [{ + role: "filer", + party_type: "appellant", + first_name: "Example", + last_name: "Filer" + }], + lower_court_code: "1753", + lower_court_prod_code: "6982", + lower_court_name: "Brighton Division, Boston Municipal Court", + lower_court_title: "Example v. Test", + lower_court_docket_number: "24-CV-001", + lower_court_judge: "Judge Example" + }; + const userData = handler.userDataFromCaseData(caseData); + const result = handler.buildEFilingData(userData, caseData, {}, "pay-1"); + assert.deepStrictEqual(result.lower_court_case, { + title: "Example v. Test", + docket_number: "24-CV-001", + judge: "Judge Example" + }); + assert.deepStrictEqual(result.trial_court, { + name: caseData.lower_court_name, + tyler_lower_court_code: "1753", + tyler_prod_lower_court_code: "6982" + }); + assert.strictEqual(result.docket_number, undefined); + assert.strictEqual(result.user_started_case, true); + + caseData.existing_case = "existing"; + caseData.previous_case_id = "court-case-id"; + const existing = handler.buildEFilingData(userData, caseData, {}, "pay-1"); + assert.strictEqual(existing.lower_court_case, undefined); + assert.strictEqual(existing.trial_court, undefined); }); \ No newline at end of file diff --git a/efile_app/playwright.appeal.config.js b/efile_app/playwright.appeal.config.js new file mode 100644 index 00000000..bff352a7 --- /dev/null +++ b/efile_app/playwright.appeal.config.js @@ -0,0 +1,17 @@ +const { + defineConfig, + devices +} = require('@playwright/test'); + +module.exports = defineConfig({ + testDir: './tests', + testMatch: 'appeal-new-case.spec.js', + fullyParallel: false, + workers: 1, + reporter: 'list', + timeout: 120000, + use: { + baseURL: process.env.E2E_TEST_BASE_URL || 'http://127.0.0.1:8001', + ...devices['Desktop Chrome'], + }, +}); \ No newline at end of file diff --git a/efile_app/tests/appeal-efsp-stub.py b/efile_app/tests/appeal-efsp-stub.py new file mode 100644 index 00000000..bd00ea4d --- /dev/null +++ b/efile_app/tests/appeal-efsp-stub.py @@ -0,0 +1,57 @@ +"""Local read-only EFSP code lists for the appellate browser regression. + +Run with ``python tests/appeal-efsp-stub.py``. No fee or filing endpoints are +implemented; requests to them fail, so the browser can never submit a filing. +""" + +import json +from http.server import BaseHTTPRequestHandler, HTTPServer + + +class Handler(BaseHTTPRequestHandler): + def do_GET(self): + if self.path.startswith("/jurisdictions/illinois/codes/courts/TAC1/categories"): + data = [{"code": "21083", "name": "Appeal", "ecfcasetype": "AppellateCase"}] + elif self.path.startswith("/jurisdictions/massachusetts/codes/courts/appeals:acp/categories"): + # Captured from the public Suffolk test EFSP endpoint on 2026-10-01. + data = [ + {"code": "5796", "name": "Appeals Court Panel Cases - Civil", "ecfcasetype": "AppellateCase"}, + {"code": "5797", "name": "Appeals Court Panel Cases - Criminal", "ecfcasetype": "AppellateCase"}, + ] + elif self.path.startswith("/jurisdictions/illinois/codes/courts/?"): + data = [ + {"code": "adams", "name": "Adams County"}, + {"code": "cook:law1", "name": "Cook County - Law - District 1 - Chicago"}, + {"code": "TAC1", "name": "Appellate Court - 1st District"}, + {"code": "zdev-test", "name": "Z - test court"}, + ] + elif self.path.startswith("/jurisdictions/illinois/codes/courts/TAC1/case_types/37653/party_types"): + data = [ + {"code": "plaintiff", "name": "Plaintiff", "isrequired": "true"}, + {"code": "defendant", "name": "Defendant", "isrequired": "true"}, + ] + elif self.path.startswith("/jurisdictions/massachusetts/codes/courts/appeals:acp/case_types/7660/party_types"): + data = [ + {"code": "plaintiff", "name": "Plaintiff", "isrequired": "true"}, + {"code": "defendant", "name": "Defendant", "isrequired": "true"}, + ] + else: + self.send_response(404) + self.end_headers() + self.wfile.write(b"not found") + return + + body = json.dumps(data).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def log_message(self, format_string, *args): + print(format_string % args, flush=True) + + +if __name__ == "__main__": + print("Serving read-only appellate EFSP stubs at http://127.0.0.1:8999", flush=True) + HTTPServer(("127.0.0.1", 8999), Handler).serve_forever() diff --git a/efile_app/tests/appeal-new-case.spec.js b/efile_app/tests/appeal-new-case.spec.js new file mode 100644 index 00000000..77575ac3 --- /dev/null +++ b/efile_app/tests/appeal-new-case.spec.js @@ -0,0 +1,105 @@ +/** + * Browser regression for appellate case metadata (#266). + * + * In four terminals from efile_app, make a disposable database and run: + * python tests/appeal-efsp-stub.py + * UV_CACHE_DIR=/tmp/uv-cache DATABASE_URL=sqlite:////tmp/litefile-appeal-e2e.sqlite3 EFSP_URL=http://127.0.0.1:8999 uv run python manage.py migrate + * UV_CACHE_DIR=/tmp/uv-cache DATABASE_URL=sqlite:////tmp/litefile-appeal-e2e.sqlite3 EFSP_URL=http://127.0.0.1:8999 uv run python manage.py seed_accessibility_session --appeal --jurisdiction illinois --output /tmp/litefile-il-e2e.json --origin http://127.0.0.1:8001 + * # Seed Massachusetts in the same database using --jurisdiction massachusetts and a second output path. + * UV_CACHE_DIR=/tmp/uv-cache DATABASE_URL=sqlite:////tmp/litefile-appeal-e2e.sqlite3 EFSP_URL=http://127.0.0.1:8999 uv run python manage.py runserver 127.0.0.1:8001 --noreload + * + * Then run against that local session and stub: + * APPEAL_TEST_STORAGE_STATE=/tmp/litefile-il-e2e.json APPEAL_TEST_JURISDICTION=illinois \ + * E2E_TEST_BASE_URL=http://127.0.0.1:8001 \ + * npx playwright test --config=playwright.appeal.config.js + * # Repeat with the Massachusetts storage state and APPEAL_TEST_JURISDICTION=massachusetts. + * + * The stub implements no fee or submission endpoint. The test stops at Review + * and never clicks the submission button. + */ +const { + test, + expect +} = require('@playwright/test'); + +const storageState = process.env.APPEAL_TEST_STORAGE_STATE; +const jurisdiction = process.env.APPEAL_TEST_JURISDICTION || 'illinois'; +if (storageState) test.use({ + storageState +}); + +test.skip(!storageState, 'Set APPEAL_TEST_STORAGE_STATE to a seeded local session.'); + +test(`new ${jurisdiction} appeal requires a valid lower court and shows it on Review`, async ({ + page +}) => { + const base = `/jurisdiction/${jurisdiction}`; + await page.goto(`${base}/parties/`, { + waitUntil: 'networkidle' + }); + + // Moving on from the people step must route an AppellateCase through the + // new lower-court questions before fees. + await expect(page.locator('input[name="filer_party_type"][value="plaintiff"]')).toBeChecked(); + await page.getByRole('button', { + name: /Continue/i + }).click(); + await expect(page).toHaveURL(new RegExp(`/jurisdiction/${jurisdiction}/case-questions/`)); + + const lowerCourt = page.locator('select[name="lower_court_code"]'); + await expect(lowerCourt).toHaveAttribute('required', ''); + const lowerCourtOptions = await lowerCourt.locator('option').evaluateAll(options => + options.filter(option => option.value).map(option => ({ + value: option.value, + label: option.textContent.trim() + })) + ); + if (jurisdiction === 'massachusetts') { + expect(lowerCourtOptions.length).toBeGreaterThan(100); + expect(lowerCourtOptions).toContainEqual({ + value: '1764', + label: 'Attleboro District Court' + }); + expect(lowerCourtOptions.some(option => /Appeals Court|Supreme Judicial Court/i.test(option.label))).toBe(false); + } else { + expect(lowerCourtOptions).toContainEqual({ + value: 'cook:law1', + label: 'Cook County - Law - District 1 - Chicago' + }); + expect(lowerCourtOptions).toContainEqual({ + value: 'adams', + label: 'Adams County' + }); + expect(lowerCourtOptions.some(option => option.value === 'TAC1' || option.value === 'zdev-test')).toBe(false); + } + + const selectedCode = jurisdiction === 'massachusetts' ? '1764' : 'cook:law1'; + const selectedLabel = jurisdiction === 'massachusetts' ? 'Attleboro District Court' : 'Cook County - Law - District 1 - Chicago'; + await lowerCourt.selectOption(selectedCode); + await page.locator('[name="lower_court_docket_number"]').fill('2026-CH-00421'); + await page.locator('[name="lower_court_title"]').fill('Reed v. Sample Company'); + await page.locator('[name="lower_court_judge"]').fill('Hon. Morgan Judge'); + await page.getByRole('button', { + name: /Continue to fees/i + }).click(); + await expect(page).toHaveURL(new RegExp(`/jurisdiction/${jurisdiction}/payment/`)); + + // This is the final pre-submission screen. Navigate there directly so the + // test remains independent of payment-provider behavior. + await page.goto(`${base}/review/`); + await expect(page.getByRole('heading', { + name: 'Review your filing' + })).toBeVisible(); + await expect(page.getByText(selectedLabel)).toBeVisible(); + await expect(page.getByText('2026-CH-00421')).toBeVisible(); + await expect(page.getByText('Reed v. Sample Company')).toBeVisible(); + await expect(page.getByText('Hon. Morgan Judge')).toBeVisible(); + await expect(page.getByRole('button', { + name: /Submit(?: and pay)?/i + })).toBeVisible(); + await page.getByRole('link', { + name: 'Edit case questions' + }).click(); + await expect(lowerCourt).toHaveValue(selectedCode); + await expect(page.locator('[name="lower_court_docket_number"]')).toHaveValue('2026-CH-00421'); +}); \ No newline at end of file diff --git a/efile_app/uv.lock b/efile_app/uv.lock index 69535218..debe4276 100644 --- a/efile_app/uv.lock +++ b/efile_app/uv.lock @@ -1829,11 +1829,11 @@ wheels = [ [[package]] name = "pypdf" -version = "6.16.1" +version = "6.19.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/5a/df92d1c1ef8806ca28f20f978ee059894868d93de797a7e2edebe7fe1a43/pypdf-6.16.1.tar.gz", hash = "sha256:c4d1b43ddae921387321cf63936cd16a7743b91d2da92f165c149a195c972ba9", size = 7003737, upload-time = "2026-08-14T12:24:04.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/ac/63d71aaedb59acbcdef491e6ca6469165e3771c9c74358204818fd9bc5a6/pypdf-6.19.0.tar.gz", hash = "sha256:bbc43aca292369ccc6cbc8a921991ecf2538a3587ab5a116eff06c321d647155", size = 7033266, upload-time = "2026-09-16T09:32:05.946Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/a1/724b18d6757ab7253a8fecd3a430eb8d980ed26872ba16651e7b5ddfc63f/pypdf-6.16.1-py3-none-any.whl", hash = "sha256:63fec31c4092ae50b6729beedcb469055b60d20c834bde1c402df241f371f644", size = 382924, upload-time = "2026-08-14T12:24:02.854Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2c/c43c03eaf630435f023f1dc61ec4a4a78951ad5530a62c71cc89bde307b7/pypdf-6.19.0-py3-none-any.whl", hash = "sha256:7e5d6e730e7dae87d560a2cee218b852f6498c8be61966f3cd02ead971e48d14", size = 395480, upload-time = "2026-09-16T09:32:04.087Z" }, ] [[package]]