Skip to main content

qualia_client_core/wellfair/api/
welfare_work.rs

1//! Welfare support + cooperative work items
2
3use super::super::blob_store::BlobStore;
4use super::super::journal::JournalEntry;
5use qualia_cooperative_core::work_item::{
6    build_work_item_envelope, build_work_item_status_envelope, derive_board,
7    parse_work_item_status_summary, parse_work_item_summary, work_item_status_summary,
8    work_item_summary, BoardColumn, WorkItem, WorkItemStatusEvent,
9};
10use wellfare_core::authority_attestation::{
11    authority_attestation_summary, build_authority_attestation_envelope, AgentInCapacity,
12    Authority, AuthorityAttestation, Representation,
13};
14use wellfare_core::welfare_support::{
15    build_assistance_need_envelope, build_government_letter_envelope,
16    build_welfare_stream_envelope, AssistanceNeed, GovernmentLetter, StreamStatus, Urgency,
17    WelfareStream,
18};
19
20use super::*;
21
22impl WebizenHostApi {
23    // --- Welfare support (Phase 3 / LIF-08..) ---
24
25    pub fn add_assistance_need(
26        &mut self,
27        category: &str,
28        description: &str,
29        urgency: Urgency,
30    ) -> Result<JournalEntry, String> {
31        let mut need = AssistanceNeed::new(category, description, Self::now_unix() as u32);
32        need.urgency = urgency;
33        let hash =
34            Self::payload_hash_hex(&serde_json::to_string(&need).map_err(|e| e.to_string())?);
35        let asserted = Self::now_unix() as u32;
36        let envelope = build_assistance_need_envelope(
37            &need,
38            &self.owner_did,
39            &self.author_did,
40            asserted,
41            Some(hash),
42        );
43        let summary = wellfare_core::welfare_support::assistance_need_summary(&need);
44        self.submit_record_with_summary(QAPP_WELFARE, envelope, SOURCE_WELFARE, Some(summary))?;
45        self.finalize_batch().ok();
46        self.latest_journal_entry()
47    }
48
49    pub fn add_welfare_stream(
50        &mut self,
51        program_name: &str,
52        reference: Option<String>,
53        status: StreamStatus,
54    ) -> Result<JournalEntry, String> {
55        let mut stream = WelfareStream::new(program_name, Self::now_unix() as u32);
56        stream.reference = reference.filter(|s| !s.is_empty());
57        stream.status = status;
58        let hash =
59            Self::payload_hash_hex(&serde_json::to_string(&stream).map_err(|e| e.to_string())?);
60        let asserted = Self::now_unix() as u32;
61        let envelope = build_welfare_stream_envelope(
62            &stream,
63            &self.owner_did,
64            &self.author_did,
65            asserted,
66            Some(hash),
67        );
68        let summary = wellfare_core::welfare_support::welfare_stream_summary(&stream);
69        self.submit_record_with_summary(QAPP_WELFARE, envelope, SOURCE_WELFARE, Some(summary))?;
70        self.finalize_batch().ok();
71        self.latest_journal_entry()
72    }
73
74    pub fn add_government_letter(
75        &mut self,
76        sender: &str,
77        subject: &str,
78        action_required: bool,
79    ) -> Result<JournalEntry, String> {
80        let mut letter = GovernmentLetter::new(sender, subject, Self::now_unix() as u32);
81        letter.action_required = action_required;
82        let asserted = Self::now_unix() as u32;
83        let envelope =
84            build_government_letter_envelope(&letter, &self.owner_did, &self.author_did, asserted);
85        let summary = wellfare_core::welfare_support::government_letter_summary(&letter);
86        self.submit_record_with_summary(QAPP_WELFARE, envelope, SOURCE_WELFARE, Some(summary))?;
87        self.finalize_batch().ok();
88        self.latest_journal_entry()
89    }
90
91    /// Record a general **authority attestation** — the ontological generalization of a government
92    /// letter: an authorizing body (extensible type + jurisdiction + department) attested by an
93    /// agent-in-capacity, delivered as a PDF, a credential, or a PDF-with-embedded-credential.
94    /// `add_government_letter` remains a preset (`authority:government`, PDF) of this model.
95    #[allow(clippy::too_many_arguments)]
96    pub fn add_authority_attestation(
97        &mut self,
98        authority_type: &str,
99        authority_label: &str,
100        jurisdiction: Option<String>,
101        department: Option<String>,
102        agent_name: Option<String>,
103        agent_capacity: Option<String>,
104        representation: &str,
105        subject: &str,
106        statement: &str,
107        action_required: bool,
108    ) -> Result<JournalEntry, String> {
109        let issued = Self::now_unix() as u32;
110        let authority = Authority::new(authority_type, authority_label);
111        let representation = match representation.to_ascii_lowercase().as_str() {
112            "credential" => Representation::Credential,
113            "pdf_with_embedded_credential" | "both" => Representation::PdfWithEmbeddedCredential,
114            _ => Representation::Pdf,
115        };
116        let mut att = AuthorityAttestation::new(authority, subject, statement, issued)
117            .with_representation(representation)
118            .with_action_required(action_required);
119        if let Some(j) = jurisdiction {
120            att = att.with_jurisdiction(j);
121        }
122        if let Some(d) = department {
123            att = att.with_department(d);
124        }
125        if let (Some(n), Some(c)) = (agent_name, agent_capacity) {
126            att = att.with_agent(AgentInCapacity::new(n, c));
127        }
128        let envelope =
129            build_authority_attestation_envelope(&att, &self.owner_did, &self.author_did, issued);
130        let summary = authority_attestation_summary(&att);
131        self.submit_record_with_summary(QAPP_WELFARE, envelope, SOURCE_WELFARE, Some(summary))?;
132        self.finalize_batch().ok();
133        self.latest_journal_entry()
134    }
135
136    /// Record a government letter together with its document bytes (stored as a content-addressed
137    /// blob; the letter's `attachment_blob_hash` is that blob's hash, retrievable via `attachment_bytes`).
138    pub fn add_government_letter_attachment(
139        &mut self,
140        sender: &str,
141        subject: &str,
142        action_required: bool,
143        bytes: &[u8],
144    ) -> Result<JournalEntry, String> {
145        let hash = BlobStore::open(&self.storage_root)
146            .and_then(|store| store.put(bytes))
147            .map_err(|e| e.to_string())?;
148        let mut letter = GovernmentLetter::new(sender, subject, Self::now_unix() as u32);
149        letter.action_required = action_required;
150        letter.attachment_blob_hash = Some(hash);
151        let asserted = Self::now_unix() as u32;
152        let envelope =
153            build_government_letter_envelope(&letter, &self.owner_did, &self.author_did, asserted);
154        let summary = wellfare_core::welfare_support::government_letter_summary(&letter);
155        self.submit_record_with_summary(QAPP_WELFARE, envelope, SOURCE_WELFARE, Some(summary))?;
156        self.finalize_batch().ok();
157        self.latest_journal_entry()
158    }
159
160    /// All welfare-support journal rows (assistance needs, streams, government letters).
161    pub fn list_welfare_records(&self, limit: usize) -> Result<Vec<JournalEntry>, String> {
162        Ok(self
163            .list_health_records(limit)?
164            .into_iter()
165            .filter(|e| {
166                matches!(
167                    e.kind.as_str(),
168                    "assistance_need" | "welfare_stream" | "government_letter"
169                )
170            })
171            .collect())
172    }
173
174    // --- Cooperative work items (shared cooperative-core domain; plan §8, WP3) ---
175    //
176    // Work items persist through the same signed journal/policy path as WellFair records; a
177    // future dedicated cooperative service may take over persistence, but the domain types and
178    // derivations already live in `qualia-cooperative-core` so the Cooperative Qapp and the
179    // WellFair panels share one implementation.
180
181    pub fn add_work_item(&mut self, item: &WorkItem) -> Result<JournalEntry, String> {
182        let asserted = Self::now_unix() as u32;
183        let envelope = build_work_item_envelope(item, &self.owner_did, &self.author_did, asserted);
184        let summary = work_item_summary(item);
185        self.submit_record_with_summary(
186            QAPP_COOPERATIVE,
187            envelope,
188            SOURCE_COOPERATIVE,
189            Some(summary),
190        )?;
191        self.finalize_batch().ok();
192        self.latest_journal_entry()
193    }
194
195    /// Append an immutable status transition. The current status is a derived projection
196    /// (latest event), never a mutated field — so replayed transitions can't corrupt the board.
197    pub fn add_work_item_status(
198        &mut self,
199        event: &WorkItemStatusEvent,
200    ) -> Result<JournalEntry, String> {
201        let asserted = Self::now_unix() as u32;
202        let envelope =
203            build_work_item_status_envelope(event, &self.owner_did, &self.author_did, asserted);
204        let summary = work_item_status_summary(event);
205        self.submit_record_with_summary(
206            QAPP_COOPERATIVE,
207            envelope,
208            SOURCE_COOPERATIVE,
209            Some(summary),
210        )?;
211        self.finalize_batch().ok();
212        self.latest_journal_entry()
213    }
214
215    pub fn list_work_items(&self, limit: usize) -> Result<Vec<JournalEntry>, String> {
216        self.list_journal_by_kind("work_item", limit)
217    }
218
219    /// Derive the Kanban board for a project from committed work items and their status events.
220    /// Pure over the unique-event-id set, so duplicate/replayed transitions never mis-place a card.
221    pub fn work_item_board(
222        &self,
223        project_id: &str,
224        limit: usize,
225    ) -> Result<Vec<BoardColumn>, String> {
226        let rows = self.list_health_records(limit)?;
227        let mut items = Vec::new();
228        let mut events = Vec::new();
229        for row in rows {
230            let Some(ref summary) = row.summary else {
231                continue;
232            };
233            match row.kind.as_str() {
234                "work_item" => {
235                    if let Some(item) = parse_work_item_summary(summary) {
236                        if item.project_id == project_id {
237                            items.push(item);
238                        }
239                    }
240                }
241                "work_item_status" => {
242                    if let Some(ev) = parse_work_item_status_summary(summary) {
243                        events.push(ev);
244                    }
245                }
246                _ => {}
247            }
248        }
249        Ok(derive_board(&items, &events))
250    }
251}