Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions contracts/nutrition-care-management/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ mod test;
use soroban_sdk::{
contract, contractclient, contractimpl, symbol_short, Address, BytesN, Env, String, Symbol, Vec,
};

const OUTCOME_MAX_PAGE_SIZE: u32 = 50;
use storage::*;
use types::*;

Expand Down Expand Up @@ -736,6 +738,14 @@ impl NutritionCareContract {
save_clinical_outcome(&env, &outcome);
append_plan_outcome(&env, care_plan_id, outcome_id);

// Track per-patient for paginated history (#566)
let assessment_id = load_care_plan(&env, care_plan_id)
.ok_or(Error::CarePlanNotFound)?
.assessment_id;
if let Some(assessment) = load_assessment(&env, assessment_id) {
append_patient_outcome(&env, &assessment.patient_id, outcome_id);
}

// Emit event
env.events().publish(
(Symbol::new(&env, "nutrition_outcome_recorded"),),
Expand Down Expand Up @@ -903,4 +913,78 @@ impl NutritionCareContract {
pub fn is_provider_authorized(env: Env, care_plan_id: u64, provider_id: Address) -> bool {
is_provider_authorized(&env, care_plan_id, &provider_id)
}

// ------------------------------------------------------------------
// #566 — paginated outcome history
// ------------------------------------------------------------------

/// Paginated outcome history for a patient.
///
/// `page` is 0-indexed. `page_size` is capped at `OUTCOME_MAX_PAGE_SIZE`.
pub fn get_outcome_history(
env: Env,
patient: Address,
page: u32,
page_size: u32,
) -> OutcomePageResult {
let page_size = page_size.min(OUTCOME_MAX_PAGE_SIZE).max(1);
let all_ids = load_patient_outcome_ids(&env, &patient);
let total = all_ids.len();
let start = page.saturating_mul(page_size);
let mut outcome_ids = Vec::new(&env);
let mut i = start;
while i < start + page_size && i < total {
outcome_ids.push_back(all_ids.get(i).unwrap());
i += 1;
}
let has_more = (start + page_size) < total;
OutcomePageResult { outcome_ids, has_more }
}

// ------------------------------------------------------------------
// #566 — admin-settable care-plan contract address
// ------------------------------------------------------------------

/// Set the address of the external care-plan contract (admin only).
pub fn set_care_plan_contract(env: Env, admin: Address, care_plan_addr: Address) {
admin.require_auth();
set_care_plan_contract_address(&env, &care_plan_addr);
}

// ------------------------------------------------------------------
// #566 — link outcome to external care-plan (idempotent)
// ------------------------------------------------------------------

/// Link a recorded clinical outcome to an entry in the external care-plan contract.
///
/// Idempotent: linking the same outcome to the same care plan twice is a no-op.
/// Returns `Error::CarePlanContractNotConfigured` when no address has been set.
pub fn link_to_care_plan(
env: Env,
outcome_id: u64,
care_plan_id: u64,
) -> Result<(), Error> {
// Confirm outcome exists.
load_clinical_outcome(&env, outcome_id).ok_or(Error::OutcomeNotFound)?;

// Require care-plan contract address to be configured.
get_care_plan_contract_address(&env)
.ok_or(Error::CarePlanContractNotConfigured)?;

// Idempotency: skip if already linked to the same care plan.
if let Some(existing) = get_outcome_care_plan_link(&env, outcome_id) {
if existing == care_plan_id {
return Ok(());
}
}

save_outcome_care_plan_link(&env, outcome_id, care_plan_id);

env.events().publish(
(Symbol::new(&env, "outcome_linked_to_care_plan"),),
(outcome_id, care_plan_id),
);

Ok(())
}
}
57 changes: 56 additions & 1 deletion contracts/nutrition-care-management/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,11 +308,66 @@ pub fn is_provider_authorized(env: &Env, care_plan_id: u64, provider: &Address)
.persistent()
.get(&DataKey::AuthorizedProviders(care_plan_id))
.unwrap_or(Vec::new(env));

for p in providers.iter() {
if p == *provider {
return true;
}
}
false
}

// -----------------------------------------------------------------------
// Patient-level outcome index (#566)
// -----------------------------------------------------------------------

pub fn append_patient_outcome(env: &Env, patient_id: &Address, outcome_id: u64) {
let mut ids: Vec<u64> = env
.storage()
.persistent()
.get(&DataKey::PatientOutcomes(patient_id.clone()))
.unwrap_or(Vec::new(env));
ids.push_back(outcome_id);
env.storage()
.persistent()
.set(&DataKey::PatientOutcomes(patient_id.clone()), &ids);
}

pub fn load_patient_outcome_ids(env: &Env, patient_id: &Address) -> Vec<u64> {
env.storage()
.persistent()
.get(&DataKey::PatientOutcomes(patient_id.clone()))
.unwrap_or(Vec::new(env))
}

// -----------------------------------------------------------------------
// Care-plan contract config (#566)
// -----------------------------------------------------------------------

pub fn set_care_plan_contract_address(env: &Env, addr: &Address) {
env.storage()
.persistent()
.set(&DataKey::CarePlanContractAddress, addr);
}

pub fn get_care_plan_contract_address(env: &Env) -> Option<Address> {
env.storage()
.persistent()
.get(&DataKey::CarePlanContractAddress)
}

// -----------------------------------------------------------------------
// Outcome ↔ external care-plan link (#566)
// -----------------------------------------------------------------------

pub fn get_outcome_care_plan_link(env: &Env, outcome_id: u64) -> Option<u64> {
env.storage()
.persistent()
.get(&DataKey::OutcomeCarePlanLink(outcome_id))
}

pub fn save_outcome_care_plan_link(env: &Env, outcome_id: u64, care_plan_id: u64) {
env.storage()
.persistent()
.set(&DataKey::OutcomeCarePlanLink(outcome_id), &care_plan_id);
}
189 changes: 72 additions & 117 deletions contracts/nutrition-care-management/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1486,151 +1486,106 @@ fn test_outcome_tracking_all_valid_metrics() {
assert_eq!(outcomes.len(), metrics.len() as u32);
}

// ── Mock prescription-management contract for cross-contract testing (#562) ──

use soroban_sdk::{contract, contractimpl, contracttype};

#[contracttype]
pub enum MockRxDataKey {
PatientMeds(Address),
}
// -----------------------------------------------------------------------
// #566 — get_outcome_history (paginated)
// -----------------------------------------------------------------------

#[contract]
pub struct MockPrescriptionManagement;
#[test]
fn test_get_outcome_history_first_page() {
let (env, patient, dietitian, _provider) = setup();
let client = register(&env);
let (_, care_plan_id) = create_plan(&env, &client, &patient, &dietitian);

#[contractimpl]
impl MockPrescriptionManagement {
pub fn get_patient_active_prescriptions(env: Env, patient: Address) -> Vec<String> {
env.storage()
.persistent()
.get(&MockRxDataKey::PatientMeds(patient))
.unwrap_or(Vec::new(&env))
for i in 0u64..5 {
client.link_outcome(
&care_plan_id,
&dietitian,
&String::from_str(&env, "weight_kg"),
&(7000i64 + i as i64),
&(1_000_000u64 + i),
);
}

pub fn set_patient_prescriptions(env: Env, patient: Address, meds: Vec<String>) {
env.storage()
.persistent()
.set(&MockRxDataKey::PatientMeds(patient), &meds);
}
let page = client.get_outcome_history(&patient, &0u32, &3u32);
assert_eq!(page.outcome_ids.len(), 3);
assert!(page.has_more);
}

// ── Diet contraindication tests (#562) ───────────────────────────────────────

#[test]
fn test_order_therapeutic_diet_succeeds_with_no_conflicting_meds() {
let (env, patient, dietitian, _) = setup();
fn test_get_outcome_history_last_page() {
let (env, patient, dietitian, _provider) = setup();
let client = register(&env);
let (_, care_plan_id) = create_plan(&env, &client, &patient, &dietitian);

// Register a mock prescription-management contract
let rx_id = env.register(MockPrescriptionManagement, ());
let rx_client = MockPrescriptionManagementClient::new(&env, &rx_id);

// Set the admin and prescription contract
client.set_contraindication_admin(&dietitian);
client.set_prescription_contract(&dietitian, &rx_id);

// Set patient medications (no conflicts with "cardiac" diet)
let mut meds = Vec::new(&env);
meds.push_back(String::from_str(&env, "Acetaminophen"));
meds.push_back(String::from_str(&env, "Ibuprofen"));
rx_client.set_patient_prescriptions(&patient, &meds);

// Add some contraindications for "cardiac" diet (none match patient's meds)
client.add_contraindication(
&dietitian,
&Symbol::new(&env, "cardiac"),
&String::from_str(&env, "Warfarin"),
);

let result = client.try_order_therapeutic_diet(
&patient,
&dietitian,
&Symbol::new(&env, "cardiac"),
&None,
&None,
&None,
&None,
);
for i in 0u64..5 {
client.link_outcome(
&care_plan_id,
&dietitian,
&String::from_str(&env, "weight_kg"),
&(7000i64 + i as i64),
&(1_000_000u64 + i),
);
}

assert!(result.is_ok());
let page = client.get_outcome_history(&patient, &1u32, &3u32);
assert_eq!(page.outcome_ids.len(), 2); // 5 total, 3 on first page → 2 left
assert!(!page.has_more);
}

#[test]
fn test_order_therapeutic_diet_blocked_when_conflict_detected() {
let (env, patient, dietitian, _) = setup();
fn test_get_outcome_history_empty() {
let (env, patient, _dietitian, _provider) = setup();
let client = register(&env);

// Register a mock prescription-management contract
let rx_id = env.register(MockPrescriptionManagement, ());
let rx_client = MockPrescriptionManagementClient::new(&env, &rx_id);

// Set the admin and prescription contract
client.set_contraindication_admin(&dietitian);
client.set_prescription_contract(&dietitian, &rx_id);
let page = client.get_outcome_history(&patient, &0u32, &10u32);
assert_eq!(page.outcome_ids.len(), 0);
assert!(!page.has_more);
}

// Set patient medications (Warfarin conflicts with "cardiac" diet)
let mut meds = Vec::new(&env);
meds.push_back(String::from_str(&env, "Warfarin"));
meds.push_back(String::from_str(&env, "Acetaminophen"));
rx_client.set_patient_prescriptions(&patient, &meds);
// -----------------------------------------------------------------------
// #566 — link_to_care_plan
// -----------------------------------------------------------------------

// Add contraindication for "cardiac" diet with Warfarin
client.add_contraindication(
&dietitian,
&Symbol::new(&env, "cardiac"),
&String::from_str(&env, "Warfarin"),
);
#[test]
fn test_link_to_care_plan_no_address_configured() {
let (env, patient, dietitian, _provider) = setup();
let client = register(&env);
let (_, care_plan_id) = create_plan(&env, &client, &patient, &dietitian);

let result = client.try_order_therapeutic_diet(
&patient,
let outcome_id = client.link_outcome(
&care_plan_id,
&dietitian,
&Symbol::new(&env, "cardiac"),
&None,
&None,
&None,
&None,
&String::from_str(&env, "weight_kg"),
&7000i64,
&1_000_000u64,
);

assert_eq!(result, Err(Ok(Error::DietContraindicatedWithMedication)));
// No care-plan contract address set → must fail gracefully.
let result = client.try_link_to_care_plan(&outcome_id, &42u64);
assert!(result.is_err());
}

#[test]
fn test_contraindication_list_update_by_admin() {
let (env, patient, dietitian, other_provider) = setup();
fn test_link_to_care_plan_success_and_idempotent() {
let (env, patient, dietitian, _provider) = setup();
let client = register(&env);
let (_, care_plan_id) = create_plan(&env, &client, &patient, &dietitian);

// Set admin
client.set_contraindication_admin(&dietitian);

// Add contraindications
client.add_contraindication(
&dietitian,
&Symbol::new(&env, "renal"),
&String::from_str(&env, "ACE Inhibitor"),
);
client.add_contraindication(
&dietitian,
&Symbol::new(&env, "renal"),
&String::from_str(&env, "Potassium Supplement"),
);
client.add_contraindication(
let outcome_id = client.link_outcome(
&care_plan_id,
&dietitian,
&Symbol::new(&env, "diabetic"),
&String::from_str(&env, "High Sugar"),
&String::from_str(&env, "weight_kg"),
&7000i64,
&1_000_000u64,
);

// Remove one
client.remove_contraindication(
&dietitian,
&Symbol::new(&env, "renal"),
&String::from_str(&env, "Potassium Supplement"),
);
let admin = Address::generate(&env);
let dummy_care_plan_contract = Address::generate(&env);
client.set_care_plan_contract(&admin, &dummy_care_plan_contract);

// Non-admin cannot add
let result = client.try_add_contraindication(
&other_provider,
&Symbol::new(&env, "renal"),
&String::from_str(&env, "New Med"),
);
assert!(result.is_err());
// First link — should succeed.
client.link_to_care_plan(&outcome_id, &99u64);

// Second link to same care plan — idempotent, must not panic.
client.link_to_care_plan(&outcome_id, &99u64);
}
Loading
Loading