Skip to main content

qualia_client_core/wellfair/api/
pwa.rs

1//! PWA package & publish
2
3use super::super::journal::JournalEntry;
4use wellfare_core::finance::{
5    build_ledger_entry_envelope, derived_balance, ledger_entry_summary, parse_ledger_summary,
6    BalanceReport, LedgerEntry,
7};
8use wellfare_core::life_records::{
9    build_case_task_envelope, build_life_event_envelope, build_welfare_case_envelope,
10    case_task_summary, life_event_summary, welfare_case_summary, CaseTaskReport, LifeEventReport,
11    WelfareCaseReport,
12};
13use wellfare_core::mental_wellbeing::{
14    build_therapy_note_envelope, build_wellbeing_observation_envelope, therapy_note_summary,
15    wellbeing_observation_summary, TherapyNote, WellbeingObservation,
16};
17
18use super::*;
19
20impl WebizenHostApi {
21    // --- WP2: Package & Publish a qapp as an installable PWA bundle (companion-PWA P0/WP2) ---
22
23    /// Author a qapp from discrete fields and write its installable PWA bundle to `target_dir`.
24    /// Returns the written (bundle-relative) file paths. Serving the bundle over a secure origin so
25    /// a phone can install it is a later stage (P1); this produces the artifact.
26    #[cfg(not(target_arch = "wasm32"))]
27    #[allow(clippy::too_many_arguments)]
28    pub fn publish_qapp_pwa(
29        &self,
30        target_dir: &str,
31        id: &str,
32        name: &str,
33        kind: &str,
34        description: &str,
35        capabilities_csv: &str,
36        wasm_filename: &str,
37    ) -> Result<Vec<String>, String> {
38        let manifest = super::super::qapp_publish::build_manifest(
39            id,
40            name,
41            kind,
42            description,
43            capabilities_csv,
44            wasm_filename,
45        );
46        super::super::qapp_publish::write_pwa_bundle(std::path::Path::new(target_dir), &manifest)
47    }
48
49    pub fn add_life_event(&mut self, report: &LifeEventReport) -> Result<JournalEntry, String> {
50        let payload = serde_json::to_string(report).map_err(|e| e.to_string())?;
51        let hash = Self::payload_hash_hex(&payload);
52        let asserted = Self::now_unix() as u32;
53        let envelope = build_life_event_envelope(
54            report,
55            &self.owner_did,
56            &self.author_did,
57            asserted,
58            Some(hash),
59        );
60        let summary = life_event_summary(report);
61        self.submit_record_with_summary(QAPP_LIFE, envelope, SOURCE_LIFE, Some(summary))?;
62        self.latest_journal_entry()
63    }
64
65    pub fn add_welfare_case(&mut self, report: &WelfareCaseReport) -> Result<JournalEntry, String> {
66        let payload = serde_json::to_string(report).map_err(|e| e.to_string())?;
67        let hash = Self::payload_hash_hex(&payload);
68        let asserted = Self::now_unix() as u32;
69        let envelope = build_welfare_case_envelope(
70            report,
71            &self.owner_did,
72            &self.author_did,
73            asserted,
74            Some(hash),
75        );
76        let summary = welfare_case_summary(report);
77        self.submit_record_with_summary(QAPP_LIFE, envelope, SOURCE_LIFE, Some(summary))?;
78        self.latest_journal_entry()
79    }
80
81    pub fn add_case_task(&mut self, report: &CaseTaskReport) -> Result<JournalEntry, String> {
82        let payload = serde_json::to_string(report).map_err(|e| e.to_string())?;
83        let hash = Self::payload_hash_hex(&payload);
84        let asserted = Self::now_unix() as u32;
85        let envelope = build_case_task_envelope(
86            report,
87            &self.owner_did,
88            &self.author_did,
89            asserted,
90            Some(hash),
91        );
92        let summary = case_task_summary(report);
93        self.submit_record_with_summary(QAPP_LIFE, envelope, SOURCE_LIFE, Some(summary))?;
94        self.latest_journal_entry()
95    }
96
97    pub fn add_wellbeing_observation(
98        &mut self,
99        report: &WellbeingObservation,
100    ) -> Result<JournalEntry, String> {
101        let payload = serde_json::to_string(report).map_err(|e| e.to_string())?;
102        let hash = Self::payload_hash_hex(&payload);
103        let asserted = Self::now_unix() as u32;
104        let envelope = build_wellbeing_observation_envelope(
105            report,
106            &self.owner_did,
107            &self.author_did,
108            asserted,
109            Some(hash),
110        );
111        let summary = wellbeing_observation_summary(report);
112        self.submit_record_with_summary(QAPP_WELLBEING, envelope, SOURCE_WELLBEING, Some(summary))?;
113        self.latest_journal_entry()
114    }
115
116    pub fn add_therapy_note(&mut self, report: &TherapyNote) -> Result<JournalEntry, String> {
117        let payload = serde_json::to_string(report).map_err(|e| e.to_string())?;
118        let hash = Self::payload_hash_hex(&payload);
119        let asserted = Self::now_unix() as u32;
120        let envelope = build_therapy_note_envelope(
121            report,
122            &self.owner_did,
123            &self.author_did,
124            asserted,
125            Some(hash),
126        );
127        let summary = therapy_note_summary(report);
128        self.submit_record_with_summary(QAPP_WELLBEING, envelope, SOURCE_WELLBEING, Some(summary))?;
129        self.latest_journal_entry()
130    }
131
132    pub(crate) fn latest_journal_entry(&self) -> Result<JournalEntry, String> {
133        self.vault
134            .list_health_records(1)
135            .map_err(|e| e.to_string())?
136            .into_iter()
137            .next()
138            .ok_or_else(|| "record committed but not found in journal".to_string())
139    }
140
141    /// Record a signed personal-finance ledger entry (Phase 5 / FIN-01..).
142    pub fn add_ledger_entry(&mut self, entry: &LedgerEntry) -> Result<JournalEntry, String> {
143        let payload = serde_json::to_string(entry).map_err(|e| e.to_string())?;
144        let hash = Self::payload_hash_hex(&payload);
145        let asserted = Self::now_unix() as u32;
146        let envelope = build_ledger_entry_envelope(
147            entry,
148            &self.owner_did,
149            &self.author_did,
150            asserted,
151            Some(hash),
152        );
153        let summary = ledger_entry_summary(entry);
154        self.submit_record_with_summary(QAPP_FINANCE, envelope, SOURCE_FINANCE, Some(summary))?;
155        self.finalize_batch().ok();
156        self.latest_journal_entry()
157    }
158
159    /// List ledger journal rows (most recent first).
160    pub fn list_ledger_entries(&self, limit: usize) -> Result<Vec<JournalEntry>, String> {
161        self.list_journal_by_kind("ledger_entry", limit)
162    }
163
164    /// Derived balance across the ledger. Balances are a pure derivation over the
165    /// unique-entry-id set, so a duplicate or replayed commit can never move money (ยง17).
166    pub fn ledger_balance(&self, limit: usize) -> Result<BalanceReport, String> {
167        let rows = self.list_ledger_entries(limit)?;
168        let mut entries = Vec::with_capacity(rows.len());
169        for row in rows {
170            if let Some(ref summary) = row.summary {
171                if let Some((amount_cents, currency)) = parse_ledger_summary(summary) {
172                    entries.push(LedgerEntry {
173                        id: row.id.clone(),
174                        description: String::new(),
175                        amount_cents,
176                        currency,
177                        category: None,
178                        counterparty: None,
179                        project_id: None,
180                        occurred_at_unix: row.asserted_time_unix,
181                    });
182                }
183            }
184        }
185        Ok(derived_balance(&entries))
186    }
187}