1use crate::return_inputs::SCHEMA_VERSION;
9use crate::{CliError, Session};
10use btctax_core::tax::return_inputs::ReturnInputs;
11use btctax_core::tax::return_refuse::{screen_inputs, Refusal};
12use btctax_core::tax::tables::{FullReturnParams, TaxTable};
13use rusqlite::Connection;
14
15pub fn init_draft_table(conn: &Connection) -> Result<(), CliError> {
17 conn.execute(
18 "CREATE TABLE IF NOT EXISTS return_inputs_draft (\
19 year INTEGER PRIMARY KEY, inputs_json TEXT NOT NULL, \
20 schema_version INTEGER NOT NULL DEFAULT 0, parked INTEGER NOT NULL DEFAULT 0)",
21 [],
22 )?;
23 Ok(())
24}
25
26pub(crate) struct DraftRow {
27 pub ri: ReturnInputs,
28 pub version: i64,
29 pub parked: bool,
30}
31
32pub(crate) fn get_draft_row(conn: &Connection, year: i32) -> Result<Option<DraftRow>, CliError> {
34 init_draft_table(conn)?;
35 let row = conn.query_row(
36 "SELECT inputs_json, schema_version, parked FROM return_inputs_draft WHERE year=?1",
37 [year],
38 |r| {
39 Ok((
40 r.get::<_, String>(0)?,
41 r.get::<_, i64>(1)?,
42 r.get::<_, i64>(2)?,
43 ))
44 },
45 );
46 match row {
47 Ok((json, version, parked)) => {
48 let ri: ReturnInputs =
51 serde_json::from_str(&json).map_err(|e| CliError::BadConfigValue {
52 key: format!("return_inputs_draft[{year}]"),
53 value: format!("invalid JSON: {e}"),
54 })?;
55 Ok(Some(DraftRow {
56 ri,
57 version,
58 parked: parked != 0,
59 }))
60 }
61 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
62 Err(e) => Err(e.into()),
63 }
64}
65
66pub(crate) fn set_draft_row(
67 conn: &Connection,
68 year: i32,
69 ri: &ReturnInputs,
70 parked: bool,
71) -> Result<(), CliError> {
72 init_draft_table(conn)?;
73 let j = serde_json::to_string(ri).map_err(|e| CliError::BadConfigValue {
75 key: format!("return_inputs_draft[{year}]"),
76 value: format!("could not serialize: {e}"),
77 })?;
78 conn.execute(
79 "INSERT INTO return_inputs_draft(year,inputs_json,schema_version,parked) VALUES(?1,?2,?3,?4) \
80 ON CONFLICT(year) DO UPDATE SET inputs_json=?2, schema_version=?3, parked=?4",
81 rusqlite::params![year, j, SCHEMA_VERSION, parked as i64],
82 )?;
83 Ok(())
84}
85
86pub(crate) fn delete_draft(conn: &Connection, year: i32) -> Result<bool, CliError> {
87 init_draft_table(conn)?;
88 Ok(conn.execute("DELETE FROM return_inputs_draft WHERE year=?1", [year])? > 0)
89}
90
91pub fn draft_exists(conn: &Connection, year: i32) -> Result<bool, CliError> {
92 init_draft_table(conn)?;
93 match conn.query_row(
96 "SELECT 1 FROM return_inputs_draft WHERE year=?1",
97 [year],
98 |_| Ok(()),
99 ) {
100 Ok(()) => Ok(true),
101 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(false),
102 Err(e) => Err(e.into()),
103 }
104}
105
106pub(crate) fn parked_flag(conn: &Connection, year: i32) -> Result<Option<bool>, CliError> {
107 init_draft_table(conn)?;
108 match conn.query_row(
109 "SELECT parked FROM return_inputs_draft WHERE year=?1",
110 [year],
111 |r| r.get::<_, i64>(0),
112 ) {
113 Ok(p) => Ok(Some(p != 0)),
114 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
115 Err(e) => Err(e.into()),
116 }
117}
118
119pub fn save_draft(sess: &mut Session, year: i32, ri: &ReturnInputs) -> Result<(), CliError> {
127 let parked = parked_flag(sess.conn(), year)?.unwrap_or(false); set_draft_row(sess.conn(), year, ri, parked)?;
129 sess.save()?; Ok(())
131}
132
133pub enum Loaded {
139 Draft { ri: ReturnInputs, parked: bool },
140 Committed(ReturnInputs),
141 Fresh,
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct StaleNote {
150 pub year: i32,
151 pub found: i64,
152 pub expected: i64,
153}
154
155impl std::fmt::Display for StaleNote {
156 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157 write!(
158 f,
159 "discarded a stale draft for {} (schema v{}, expected v{})",
160 self.year, self.found, self.expected
161 )
162 }
163}
164
165pub fn load(conn: &Connection, year: i32) -> Result<(Loaded, Option<StaleNote>), CliError> {
181 if let Some(d) = get_draft_row(conn, year)? {
182 if d.version != SCHEMA_VERSION {
183 if d.parked {
184 return Err(CliError::StaleParkedDraft {
185 year,
186 found: d.version,
187 expected: SCHEMA_VERSION,
188 });
189 }
190 delete_draft(conn, year)?;
193 let note = StaleNote {
194 year,
195 found: d.version,
196 expected: SCHEMA_VERSION,
197 };
198 return Ok((committed_or_fresh(conn, year)?, Some(note)));
199 } else {
200 return Ok((
201 Loaded::Draft {
202 ri: d.ri,
203 parked: d.parked,
204 },
205 None,
206 ));
207 }
208 }
209 Ok((committed_or_fresh(conn, year)?, None))
210}
211
212fn committed_or_fresh(conn: &Connection, year: i32) -> Result<Loaded, CliError> {
214 match crate::return_inputs::get(conn, year)? {
215 Some(ri) => Ok(Loaded::Committed(ri)),
216 None => Ok(Loaded::Fresh),
217 }
218}
219
220pub fn coherence_clear_or_refuse(conn: &Connection, year: i32) -> Result<(), CliError> {
239 match parked_flag(conn, year)? {
240 None => Ok(()),
241 Some(true) => Err(CliError::ParkedDraftBlocksWrite { year }), Some(false) => {
243 if let Some(d) = get_draft_row(conn, year)? {
244 if d.ri != ReturnInputs::default() {
245 eprintln!(
246 "note: superseding a work-in-progress draft for {year} with this write."
247 );
248 }
249 }
250 delete_draft(conn, year)?;
251 Ok(())
252 }
253 }
254}
255
256pub enum CommitOutcome {
265 Committed,
266 Refused(Refusal),
267 NoTables,
268}
269
270fn mutate_and_save(
275 sess: &mut Session,
276 mutate: impl FnOnce(&Connection) -> Result<(), CliError>,
277) -> Result<(), CliError> {
278 let snap = sess.snapshot()?;
279 let mutated = mutate(sess.conn());
280 let outcome = mutated.and_then(|()| sess.save());
281 if let Err(e) = outcome {
282 sess.restore(&snap)?; return Err(e);
284 }
285 Ok(())
286}
287
288pub fn commit(
303 sess: &mut Session,
304 year: i32,
305 ri: &ReturnInputs,
306 table: Option<&TaxTable>,
307 params: Option<&FullReturnParams>,
308) -> Result<CommitOutcome, CliError> {
309 let (Some(table), Some(params)) = (table, params) else {
310 return Ok(CommitOutcome::NoTables); };
312 if table.year != year || params.year != year {
313 return Ok(CommitOutcome::NoTables);
316 }
317 if let Some(refusal) = screen_inputs(ri, table, params) {
318 return Ok(CommitOutcome::Refused(refusal)); }
320 mutate_and_save(sess, |conn| {
321 crate::return_inputs::set(conn, year, ri)?;
322 delete_draft(conn, year)?;
323 Ok(())
324 })?;
325 Ok(CommitOutcome::Committed)
326}
327
328pub fn park_to_profile(sess: &mut Session, year: i32) -> Result<(), CliError> {
349 let Some(ri) = crate::return_inputs::get(sess.conn(), year)? else {
350 return Err(CliError::Usage(format!(
351 "no committed return to park for {year}"
352 )));
353 };
354 if parked_flag(sess.conn(), year)? == Some(false) {
355 return Err(CliError::Usage(format!(
357 "year {year} has a work-in-progress draft; finish or discard it before switching to the tax-profile"
358 )));
359 }
360 mutate_and_save(sess, |conn| {
361 set_draft_row(conn, year, &ri, true)?; crate::return_inputs::delete(conn, year)?; Ok(())
364 }) }
366
367pub enum ActiveSource {
372 FullReturn,
373 TaxProfile,
374 Neither,
375}
376
377pub fn active_source(conn: &Connection, year: i32) -> Result<ActiveSource, CliError> {
386 if crate::return_inputs::exists(conn, year)? {
387 return Ok(ActiveSource::FullReturn);
388 }
389 if crate::tax_profile::years(conn)?.contains(&year) {
390 return Ok(ActiveSource::TaxProfile);
391 }
392 Ok(ActiveSource::Neither)
393}
394
395pub fn shadows_profile(conn: &Connection, year: i32) -> Result<bool, CliError> {
402 Ok(crate::tax_profile::years(conn)?.contains(&year))
403}
404
405pub fn discard_parked_draft(sess: &mut Session, year: i32) -> Result<(), CliError> {
416 if parked_flag(sess.conn(), year)? != Some(true) {
417 return Err(CliError::Usage(format!(
419 "year {year} has no parked draft to discard"
420 )));
421 }
422 mutate_and_save(sess, |conn| {
423 delete_draft(conn, year)?;
424 Ok(())
425 })
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431 use crate::Session;
432 use btctax_core::tax::return_inputs::ReturnInputs;
433 use btctax_core::tax::types::FilingStatus;
434 use btctax_store::Passphrase;
435 use rusqlite::Connection;
436
437 fn pp() -> Passphrase {
438 Passphrase::new("test-pass".into())
439 }
440
441 fn tmp_vault() -> (tempfile::TempDir, std::path::PathBuf, Passphrase) {
446 let dir = tempfile::tempdir().unwrap();
447 let path = dir.path().join("vault.pgp");
448 {
449 let _ = Session::create(&path, &pp()).unwrap(); }
451 (dir, path, pp())
452 }
453
454 #[test]
455 fn save_draft_preserves_parked_and_reaches_disk() {
456 let (_dir, path, pp) = tmp_vault();
457 let ri_a = ReturnInputs {
458 filing_status: FilingStatus::Single,
459 ..Default::default()
460 };
461 {
462 let mut sess = Session::open(&path, &pp).unwrap();
463 set_draft_row(sess.conn(), 2024, &ri_a, true).unwrap();
465 let ri_b = ReturnInputs {
466 filing_status: FilingStatus::Mfj,
467 ..Default::default()
468 };
469 save_draft(&mut sess, 2024, &ri_b).unwrap();
470 assert_eq!(
471 parked_flag(sess.conn(), 2024).unwrap(),
472 Some(true),
473 "NI-1: parked survives an edit"
474 );
475 }
476 let sess2 = Session::open(&path, &pp).unwrap();
478 let row = get_draft_row(sess2.conn(), 2024).unwrap().unwrap();
479 assert_eq!(row.ri.filing_status, FilingStatus::Mfj);
480 assert!(row.parked);
481 }
482
483 #[test]
484 fn save_draft_on_fresh_year_is_unparked() {
485 let (_dir, path, pp) = tmp_vault();
486 let mut sess = Session::open(&path, &pp).unwrap();
487 save_draft(&mut sess, 2024, &ReturnInputs::default()).unwrap();
488 assert_eq!(parked_flag(sess.conn(), 2024).unwrap(), Some(false));
489 }
490
491 #[test]
496 fn mutate_and_save_rolls_back_the_in_memory_write_on_a_mutate_error() {
497 let (_dir, path, pp) = tmp_vault();
498 let mut sess = Session::open(&path, &pp).unwrap();
499 assert!(
500 !draft_exists(sess.conn(), 2024).unwrap(),
501 "precondition: no draft yet"
502 );
503
504 let ri = ReturnInputs {
505 filing_status: FilingStatus::Single,
506 ..Default::default()
507 };
508 let r = mutate_and_save(&mut sess, |conn| {
509 set_draft_row(conn, 2024, &ri, false)?; Err(CliError::Usage("boom".to_string())) });
512 assert!(r.is_err(), "the mutate error must propagate");
513
514 assert!(
515 !draft_exists(sess.conn(), 2024).unwrap(),
516 "P2-d: a mutate error must restore the pre-write image; the draft write must NOT survive"
517 );
518 }
519
520 #[test]
521 fn load_precedence_draft_then_committed_then_fresh() {
522 let conn = Connection::open_in_memory().unwrap();
523 crate::return_inputs::init_table(&conn).unwrap();
524 init_draft_table(&conn).unwrap();
525 let (loaded, note) = load(&conn, 2024).unwrap();
527 assert!(matches!(loaded, Loaded::Fresh));
528 assert!(note.is_none(), "no stale discard on a fresh year");
529 let cri = ReturnInputs {
531 filing_status: FilingStatus::HoH,
532 ..Default::default()
533 };
534 crate::return_inputs::set(&conn, 2024, &cri).unwrap();
535 let (loaded, note) = load(&conn, 2024).unwrap();
536 assert!(matches!(loaded, Loaded::Committed(r) if r.filing_status == FilingStatus::HoH));
537 assert!(note.is_none(), "no stale discard on the committed path");
538 let dri = ReturnInputs {
540 filing_status: FilingStatus::Mfj,
541 ..Default::default()
542 };
543 set_draft_row(&conn, 2024, &dri, false).unwrap();
544 let (loaded, note) = load(&conn, 2024).unwrap();
545 assert!(
546 matches!(loaded, Loaded::Draft { ri, parked: false } if ri.filing_status == FilingStatus::Mfj)
547 );
548 assert!(
549 note.is_none(),
550 "no stale discard on a version-current draft"
551 );
552 }
553
554 #[test]
555 fn load_discards_stale_wip_but_refuses_stale_parked() {
556 let conn = Connection::open_in_memory().unwrap();
557 init_draft_table(&conn).unwrap();
558 let ri = ReturnInputs {
559 filing_status: FilingStatus::Single,
560 ..Default::default()
561 };
562 let j = serde_json::to_string(&ri).unwrap();
563 conn.execute("INSERT INTO return_inputs_draft(year,inputs_json,schema_version,parked) VALUES(2024,?1,0,0)", [&j]).unwrap();
566 let (loaded, note) = load(&conn, 2024).unwrap();
567 assert!(matches!(loaded, Loaded::Fresh));
568 assert_eq!(
569 note,
570 Some(StaleNote {
571 year: 2024,
572 found: 0,
573 expected: SCHEMA_VERSION
574 }),
575 "the stale-WIP discard returns the note (not an eprintln!)"
576 );
577 assert!(
578 !draft_exists(&conn, 2024).unwrap(),
579 "stale WIP is discarded"
580 );
581 conn.execute("INSERT INTO return_inputs_draft(year,inputs_json,schema_version,parked) VALUES(2025,?1,0,1)", [&j]).unwrap();
583 assert!(matches!(
584 load(&conn, 2025),
585 Err(CliError::StaleParkedDraft {
586 year: 2025,
587 found: 0,
588 ..
589 })
590 ));
591 assert!(
592 draft_exists(&conn, 2025).unwrap(),
593 "stale parked is preserved, not discarded"
594 );
595 }
596
597 fn clean_screened_ri() -> ReturnInputs {
602 let mut ri = ReturnInputs {
603 filing_status: FilingStatus::Single,
604 header: btctax_core::tax::testonly::not_a_dependent(),
605 ..Default::default()
606 };
607 btctax_core::tax::testonly::answer_all_live_declarations(&mut ri);
608 ri
609 }
610
611 #[test]
612 fn commit_non2024_is_notables_and_writes_nothing() {
613 let (_dir, path, pp) = tmp_vault();
614 let mut sess = Session::open(&path, &pp).unwrap();
615 let ri = ReturnInputs {
616 filing_status: FilingStatus::Single,
617 ..Default::default()
618 };
619 set_draft_row(sess.conn(), 2099, &ri, false).unwrap();
620 let out = commit(&mut sess, 2099, &ri, None, None).unwrap();
622 assert!(matches!(out, CommitOutcome::NoTables));
623 assert!(
624 !crate::return_inputs::exists(sess.conn(), 2099).unwrap(),
625 "NoTables writes no committed row"
626 );
627 assert!(
628 draft_exists(sess.conn(), 2099).unwrap(),
629 "NoTables leaves the draft"
630 );
631 }
632
633 #[test]
634 fn commit_clean_sets_row_and_deletes_draft_refused_writes_nothing() {
635 use btctax_adapters::{BundledFullReturnTables, BundledTaxTables};
636 use btctax_core::tax::tables::FullReturnTables;
637 use btctax_core::TaxTables;
638 let (_dir, path, pp) = tmp_vault();
639 let tables = BundledTaxTables::load(); let fr = BundledFullReturnTables::load();
641 let (t, p) = (
642 tables.table_for(2024).unwrap(),
643 fr.full_return_for(2024).unwrap(),
644 ); let mut sess = Session::open(&path, &pp).unwrap();
646 let clean = clean_screened_ri();
648 set_draft_row(sess.conn(), 2024, &clean, false).unwrap();
649 assert!(matches!(
650 commit(&mut sess, 2024, &clean, Some(t), Some(p)).unwrap(),
651 CommitOutcome::Committed
652 ));
653 assert!(
654 crate::return_inputs::exists(sess.conn(), 2024).unwrap(),
655 "clean commit writes the row"
656 );
657 assert!(
658 !draft_exists(sess.conn(), 2024).unwrap(),
659 "clean commit deletes the draft"
660 );
661 let refused = ReturnInputs {
663 filing_status: FilingStatus::Single,
664 ..Default::default()
665 }; set_draft_row(sess.conn(), 2024, &refused, false).unwrap();
667 assert!(matches!(
668 commit(&mut sess, 2024, &refused, Some(t), Some(p)).unwrap(),
669 CommitOutcome::Refused(_)
670 ));
671 assert!(
673 crate::return_inputs::exists(sess.conn(), 2024).unwrap(),
674 "a refused commit does not delete the earlier committed row"
675 );
676 assert!(
677 draft_exists(sess.conn(), 2024).unwrap(),
678 "a refused commit leaves the draft"
679 );
680 }
681
682 #[test]
686 fn commit_refuses_tables_for_a_different_year_and_writes_nothing() {
687 use btctax_adapters::{BundledFullReturnTables, BundledTaxTables};
688 use btctax_core::tax::tables::FullReturnTables;
689 use btctax_core::TaxTables;
690 let (_dir, path, pp) = tmp_vault();
691 let tables = BundledTaxTables::load(); let fr = BundledFullReturnTables::load();
693 let (t2024, p2024) = (
694 tables.table_for(2024).unwrap(),
695 fr.full_return_for(2024).unwrap(),
696 );
697 let mut sess = Session::open(&path, &pp).unwrap();
698 let clean = clean_screened_ri();
699 let out = commit(&mut sess, 2025, &clean, Some(t2024), Some(p2024)).unwrap();
701 assert!(
702 matches!(out, CommitOutcome::NoTables),
703 "tables for a different year → NoTables, not a committed write"
704 );
705 assert!(
706 !crate::return_inputs::exists(sess.conn(), 2025).unwrap(),
707 "the table-less year is never poisoned with a committed row"
708 );
709 }
710
711 #[test]
715 fn coherence_clears_wip_but_refuses_parked() {
716 let conn = Connection::open_in_memory().unwrap();
717 init_draft_table(&conn).unwrap();
718 let ri = ReturnInputs {
719 filing_status: FilingStatus::Single,
720 ..Default::default()
721 };
722 set_draft_row(&conn, 2024, &ri, false).unwrap();
724 coherence_clear_or_refuse(&conn, 2024).unwrap();
725 assert!(
726 !draft_exists(&conn, 2024).unwrap(),
727 "coherence clears a WIP draft"
728 );
729 set_draft_row(&conn, 2025, &ri, true).unwrap();
731 let err = coherence_clear_or_refuse(&conn, 2025).unwrap_err();
732 assert!(matches!(
733 err,
734 CliError::ParkedDraftBlocksWrite { year: 2025 }
735 ));
736 let msg = err.to_string();
737 assert!(
738 msg.contains("use full return") && msg.contains("discard parked draft"),
739 "M-d: names both exits"
740 );
741 assert!(
742 draft_exists(&conn, 2025).unwrap(),
743 "a parked draft is never silently destroyed"
744 );
745 coherence_clear_or_refuse(&conn, 2030).unwrap();
747 }
748
749 #[test]
750 fn park_stashes_then_deletes_committed_atomically() {
751 let (_dir, path, pp) = tmp_vault();
752 let mut sess = Session::open(&path, &pp).unwrap();
753 let ri = ReturnInputs {
754 filing_status: FilingStatus::Mfj,
755 ..Default::default()
756 };
757 crate::return_inputs::set(sess.conn(), 2024, &ri).unwrap();
758 park_to_profile(&mut sess, 2024).unwrap();
759 assert!(
761 !crate::return_inputs::exists(sess.conn(), 2024).unwrap(),
762 "park deletes the committed row"
763 );
764 let d = get_draft_row(sess.conn(), 2024).unwrap().unwrap();
765 assert!(
766 d.parked && d.ri.filing_status == FilingStatus::Mfj,
767 "park stashes the row as parked"
768 );
769 drop(sess);
771 let s2 = Session::open(&path, &pp).unwrap();
772 assert!(get_draft_row(s2.conn(), 2024).unwrap().unwrap().parked);
773 assert!(
774 !crate::return_inputs::exists(s2.conn(), 2024).unwrap(),
775 "the committed-row DELETE also reached disk, not just the stash"
776 );
777 }
778
779 #[test]
780 fn park_refuses_without_committed_row_and_on_any_wip() {
781 let (_dir, path, pp) = tmp_vault();
782 let mut sess = Session::open(&path, &pp).unwrap();
783 assert!(park_to_profile(&mut sess, 2024).is_err(), "nothing to park");
784 let ri = ReturnInputs {
785 filing_status: FilingStatus::Single,
786 ..Default::default()
787 };
788 crate::return_inputs::set(sess.conn(), 2024, &ri).unwrap();
789 set_draft_row(sess.conn(), 2024, &ri, false).unwrap(); assert!(
791 park_to_profile(&mut sess, 2024).is_err(),
792 "clean-state gate: won't clobber a WIP draft"
793 );
794 assert!(
795 crate::return_inputs::exists(sess.conn(), 2024).unwrap(),
796 "a refused park leaves the committed row"
797 );
798 }
799
800 fn sample_profile() -> btctax_core::TaxProfile {
803 use btctax_core::{Carryforward, TaxProfile};
804 use rust_decimal_macros::dec;
805 TaxProfile {
806 filing_status: FilingStatus::Mfj,
807 ordinary_taxable_income: dec!(120000),
808 magi_excluding_crypto: dec!(130000),
809 qualified_dividends_and_other_pref_income: dec!(0),
810 other_net_capital_gain: dec!(0),
811 capital_loss_carryforward_in: Carryforward {
812 short: dec!(0),
813 long: dec!(0),
814 },
815 w2_ss_wages: dec!(0),
816 w2_medicare_wages: dec!(0),
817 schedule_c_expenses: dec!(0),
818 }
819 }
820
821 #[test]
822 fn active_source_follows_resolve_precedence() {
823 let conn = Connection::open_in_memory().unwrap();
824 crate::return_inputs::init_table(&conn).unwrap();
825 crate::tax_profile::init_table(&conn).unwrap();
826 assert!(matches!(
827 active_source(&conn, 2024).unwrap(),
828 ActiveSource::Neither
829 ));
830 crate::tax_profile::set(&conn, 2024, &sample_profile()).unwrap();
831 assert!(matches!(
832 active_source(&conn, 2024).unwrap(),
833 ActiveSource::TaxProfile
834 ));
835 assert!(shadows_profile(&conn, 2024).unwrap());
836 crate::return_inputs::set(&conn, 2024, &ReturnInputs::default()).unwrap();
838 assert!(matches!(
839 active_source(&conn, 2024).unwrap(),
840 ActiveSource::FullReturn
841 ));
842 }
843
844 #[test]
845 fn discard_parked_draft_only_deletes_a_parked_row() {
846 let (_dir, path, pp) = tmp_vault();
847 let mut sess = Session::open(&path, &pp).unwrap();
848 let ri = ReturnInputs {
849 filing_status: FilingStatus::Single,
850 ..Default::default()
851 };
852 set_draft_row(sess.conn(), 2024, &ri, false).unwrap();
854 assert!(
855 discard_parked_draft(&mut sess, 2024).is_err(),
856 "won't delete a WIP behind 'discard parked'"
857 );
858 assert!(draft_exists(sess.conn(), 2024).unwrap());
859 set_draft_row(sess.conn(), 2024, &ri, true).unwrap();
861 discard_parked_draft(&mut sess, 2024).unwrap();
862 assert!(!draft_exists(sess.conn(), 2024).unwrap());
863 }
864
865 #[test]
866 fn draft_row_set_get_delete_roundtrip_with_parked() {
867 let conn = Connection::open_in_memory().unwrap();
868 init_draft_table(&conn).unwrap();
869 let ri = ReturnInputs {
870 filing_status: FilingStatus::Mfj,
871 ..Default::default()
872 };
873 set_draft_row(&conn, 2024, &ri, false).unwrap();
875 let got = get_draft_row(&conn, 2024).unwrap().unwrap();
876 assert_eq!(got.ri.filing_status, FilingStatus::Mfj);
877 assert_eq!(got.version, SCHEMA_VERSION);
878 assert!(!got.parked);
879 assert_eq!(parked_flag(&conn, 2024).unwrap(), Some(false));
880 set_draft_row(&conn, 2024, &ri, true).unwrap();
882 assert!(get_draft_row(&conn, 2024).unwrap().unwrap().parked);
883 assert!(delete_draft(&conn, 2024).unwrap());
885 assert!(get_draft_row(&conn, 2024).unwrap().is_none());
886 assert!(!delete_draft(&conn, 2024).unwrap()); }
888}