use anyhow::{Context, Result};
use camino::Utf8PathBuf;
use std::fs;
use xchecker_utils::atomic_write::write_file_atomic;
use xchecker_utils::error::XCheckerError;
use xchecker_utils::types::{PhaseId, Receipt};
use super::ReceiptManager;
impl ReceiptManager {
pub fn write_receipt(&self, receipt: &Receipt) -> Result<Utf8PathBuf> {
xchecker_utils::paths::ensure_dir_all(&self.receipts_path).with_context(|| {
format!(
"Failed to create receipts directory: {}",
self.receipts_path
)
})?;
let timestamp_str = receipt.emitted_at.format("%Y%m%d_%H%M%S").to_string();
let filename = format!("{}-{}.json", receipt.phase, timestamp_str);
let receipt_path = self.receipts_path.join(&filename);
let json_content = Self::emit_receipt_jcs(receipt)?;
write_file_atomic(&receipt_path, &json_content).map_err(|e| {
XCheckerError::ReceiptWriteFailed {
path: receipt_path.to_string(),
reason: e.to_string(),
}
})?;
Ok(receipt_path)
}
pub fn read_latest_receipt(&self, phase: PhaseId) -> Result<Option<Receipt>> {
let phase_str = phase.as_str();
if !self.receipts_path.exists() {
return Ok(None);
}
let mut phase_receipts = Vec::new();
for entry in fs::read_dir(&self.receipts_path)? {
let entry = entry?;
if let Some(filename) = entry.file_name().to_str()
&& filename.starts_with(&format!("{phase_str}-"))
&& filename.ends_with(".json")
{
phase_receipts.push(entry.path());
}
}
if phase_receipts.is_empty() {
return Ok(None);
}
phase_receipts.sort();
let latest_path = phase_receipts.last().unwrap();
let content = fs::read_to_string(latest_path)
.with_context(|| format!("Failed to read receipt: {latest_path:?}"))?;
let receipt: Receipt = serde_json::from_str(&content)
.with_context(|| format!("Failed to deserialize receipt: {latest_path:?}"))?;
Ok(Some(receipt))
}
pub fn list_receipts(&self) -> Result<Vec<Receipt>> {
if !self.receipts_path.exists() {
return Ok(Vec::new());
}
let mut receipts = Vec::new();
for entry in fs::read_dir(&self.receipts_path)? {
let entry = entry?;
if let Some(filename) = entry.file_name().to_str()
&& filename.ends_with(".json")
{
let content = fs::read_to_string(entry.path())?;
let receipt: Receipt = serde_json::from_str(&content)?;
receipts.push(receipt);
}
}
receipts.sort_by(|a, b| a.emitted_at.cmp(&b.emitted_at));
Ok(receipts)
}
#[cfg_attr(not(test), allow(dead_code))]
#[must_use]
pub const fn receipts_path(&self) -> &Utf8PathBuf {
&self.receipts_path
}
}
#[allow(dead_code)] pub fn add_rename_retry_warning(warnings: &mut Vec<String>, retry_count: Option<u32>) {
if let Some(count) = retry_count {
warnings.push(format!("rename_retry_count: {count}"));
}
}