Skip to content
Merged
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
201 changes: 178 additions & 23 deletions contracts/restaurant_registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
//! used as a foreign key in the Order and Payment contracts.
//!
//! ## Roles
//! - **Admin** – contract deployer; can deactivate any restaurant.
//! - **Admin** – contract deployer; can suspend/unsuspend any restaurant.
//! - **Owner** – the wallet that registered a restaurant; can update its
//! own restaurant metadata and toggle its active flag.
//! own restaurant metadata and permanently close it.

#![no_std]

use soroban_sdk::{contract, contractimpl, contracttype, symbol_short, Address, Env, String};
use soroban_sdk::{contract, contractimpl, contracttype, symbol_short, Address, Env, String, Symbol};

// ---------------------------------------------------------------------------
// Storage types
Expand All @@ -33,6 +33,10 @@ pub struct Restaurant {
pub is_active: bool,
/// Ledger timestamp of registration.
pub created_at: u64,
/// Address that performed the last deactivation (None if currently active).
pub deactivated_by: Option<Address>,
/// Ledger timestamp of the last deactivation (None if currently active).
pub deactivated_at: Option<u64>,
}

/// Storage key discriminants.
Expand Down Expand Up @@ -71,7 +75,7 @@ impl RestaurantRegistry {
}
env.storage().instance().set(&DataKey::Admin, &admin);
env.storage().instance().set(&DataKey::Count, &0u64);
env.storage().instance().extend_ttl(17_280, 17_280); // ~1 day
env.storage().instance().extend_ttl(17_280, 17_280);
}

// -----------------------------------------------------------------------
Expand Down Expand Up @@ -106,9 +110,11 @@ impl RestaurantRegistry {
slug: slug.clone(),
is_active: true,
created_at: env.ledger().timestamp(),
deactivated_by: None,
deactivated_at: None,
};

let ttl: u32 = 2_073_600; // ~120 days on Stellar
let ttl: u32 = 2_073_600;
env.storage()
.persistent()
.set(&DataKey::Restaurant(id), &restaurant);
Expand All @@ -126,7 +132,6 @@ impl RestaurantRegistry {
env.storage().instance().set(&DataKey::Count, &id);
env.storage().instance().extend_ttl(17_280, 17_280);

// Emit: (topic1, topic2) -> (id, owner)
env.events().publish(
(symbol_short!("register"), symbol_short!("rest")),
(id, owner, name),
Expand Down Expand Up @@ -175,10 +180,13 @@ impl RestaurantRegistry {
);
}

/// Activate or deactivate a restaurant.
/// Permanently close a restaurant. Only the restaurant's own owner may call this.
///
/// Only the owner or admin may change the active flag.
pub fn set_active(env: Env, caller: Address, restaurant_id: u64, active: bool) {
/// This action is irreversible — the admin cannot reopen a voluntarily closed restaurant.
///
/// # Panics
/// - If `caller` is not the restaurant owner.
pub fn owner_close_restaurant(env: Env, caller: Address, restaurant_id: u64) {
caller.require_auth();

let mut restaurant: Restaurant = env
Expand All @@ -187,12 +195,56 @@ impl RestaurantRegistry {
.get(&DataKey::Restaurant(restaurant_id))
.unwrap_or_else(|| panic!("restaurant not found"));

if caller != restaurant.owner {
panic!("unauthorized: only the restaurant owner can close their restaurant");
}

let now = env.ledger().timestamp();
restaurant.is_active = false;
restaurant.deactivated_by = Some(caller.clone());
restaurant.deactivated_at = Some(now);

let ttl: u32 = 2_073_600;
env.storage()
.persistent()
.set(&DataKey::Restaurant(restaurant_id), &restaurant);
env.storage()
.persistent()
.extend_ttl(&DataKey::Restaurant(restaurant_id), ttl, ttl);

env.events().publish(
(symbol_short!("rest"), symbol_short!("close")),
(restaurant_id, caller, now),
);
}

/// Suspend a restaurant (recoverable). Only the contract admin may call this.
///
/// # Panics
/// - If `caller` is not the contract admin.
pub fn admin_suspend_restaurant(
env: Env,
caller: Address,
restaurant_id: u64,
reason: Symbol,
) {
caller.require_auth();

let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
if caller != restaurant.owner && caller != admin {
panic!("unauthorized");
if caller != admin {
panic!("unauthorized: only the admin can suspend a restaurant");
}

restaurant.is_active = active;
let mut restaurant: Restaurant = env
.storage()
.persistent()
.get(&DataKey::Restaurant(restaurant_id))
.unwrap_or_else(|| panic!("restaurant not found"));

let now = env.ledger().timestamp();
restaurant.is_active = false;
restaurant.deactivated_by = Some(caller.clone());
restaurant.deactivated_at = Some(now);

let ttl: u32 = 2_073_600;
env.storage()
Expand All @@ -203,8 +255,45 @@ impl RestaurantRegistry {
.extend_ttl(&DataKey::Restaurant(restaurant_id), ttl, ttl);

env.events().publish(
(symbol_short!("setactive"), symbol_short!("rest")),
(restaurant_id, active),
(symbol_short!("rest"), symbol_short!("suspend")),
(restaurant_id, caller, now, reason),
);
}

/// Lift a suspension placed by the admin. Only the contract admin may call this.
///
/// # Panics
/// - If `caller` is not the contract admin.
pub fn admin_unsuspend_restaurant(env: Env, caller: Address, restaurant_id: u64) {
caller.require_auth();

let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
if caller != admin {
panic!("unauthorized: only the admin can unsuspend a restaurant");
}

let mut restaurant: Restaurant = env
.storage()
.persistent()
.get(&DataKey::Restaurant(restaurant_id))
.unwrap_or_else(|| panic!("restaurant not found"));

let now = env.ledger().timestamp();
restaurant.is_active = true;
restaurant.deactivated_by = None;
restaurant.deactivated_at = None;

let ttl: u32 = 2_073_600;
env.storage()
.persistent()
.set(&DataKey::Restaurant(restaurant_id), &restaurant);
env.storage()
.persistent()
.extend_ttl(&DataKey::Restaurant(restaurant_id), ttl, ttl);

env.events().publish(
(symbol_short!("rest"), symbol_short!("unsuspnd")),
(restaurant_id, caller, now),
);
}

Expand Down Expand Up @@ -257,6 +346,17 @@ mod test {
(env, client)
}

fn register_one(env: &Env, client: &RestaurantRegistryClient, admin: &Address) -> (Address, u64) {
let owner = Address::generate(env);
client.initialize(admin);
let id = client.register_restaurant(
&owner,
&String::from_str(env, "Mama's Kitchen"),
&String::from_str(env, "mamas-kitchen"),
);
(owner, id)
}

#[test]
fn test_register_and_get() {
let (env, client) = setup();
Expand All @@ -276,6 +376,8 @@ mod test {
assert_eq!(rest.owner, owner);
assert_eq!(rest.name, String::from_str(&env, "Mama's Kitchen"));
assert!(rest.is_active);
assert!(rest.deactivated_by.is_none());
assert!(rest.deactivated_at.is_none());
}

#[test]
Expand All @@ -302,24 +404,77 @@ mod test {
assert_eq!(rest.name, String::from_str(&env, "New Name"));
}

// -----------------------------------------------------------------------
// owner_close_restaurant
// -----------------------------------------------------------------------

#[test]
fn test_deactivate_restaurant() {
fn test_owner_can_close_own_restaurant() {
let (env, client) = setup();
let admin = Address::generate(&env);
let owner = Address::generate(&env);
let (owner, id) = register_one(&env, &client, &admin);

client.initialize(&admin);
let id = client.register_restaurant(
&owner,
&String::from_str(&env, "Test Rest"),
&String::from_str(&env, "test-rest"),
);
client.owner_close_restaurant(&owner, &id);

let rest = client.get_restaurant(&id);
assert!(!rest.is_active);
assert_eq!(rest.deactivated_by, Some(owner));
assert!(rest.deactivated_at.is_some());
}

#[test]
#[should_panic(expected = "unauthorized: only the restaurant owner can close their restaurant")]
fn test_admin_cannot_call_owner_close() {
let (env, client) = setup();
let admin = Address::generate(&env);
let (_owner, id) = register_one(&env, &client, &admin);

// Admin tries to use the owner-only function — must panic
client.owner_close_restaurant(&admin, &id);
}

// -----------------------------------------------------------------------
// admin_suspend_restaurant / admin_unsuspend_restaurant
// -----------------------------------------------------------------------

#[test]
fn test_admin_can_suspend_and_unsuspend() {
let (env, client) = setup();
let admin = Address::generate(&env);
let (_owner, id) = register_one(&env, &client, &admin);

let reason = Symbol::new(&env, "policy");
client.admin_suspend_restaurant(&admin, &id, &reason);

client.set_active(&admin, &id, &false);
let rest = client.get_restaurant(&id);
assert!(!rest.is_active);
assert_eq!(rest.deactivated_by, Some(admin.clone()));
assert!(rest.deactivated_at.is_some());

client.admin_unsuspend_restaurant(&admin, &id);

let rest = client.get_restaurant(&id);
assert!(rest.is_active);
assert!(rest.deactivated_by.is_none());
assert!(rest.deactivated_at.is_none());
}

#[test]
#[should_panic(expected = "unauthorized: only the admin can suspend a restaurant")]
fn test_owner_cannot_call_admin_suspend() {
let (env, client) = setup();
let admin = Address::generate(&env);
let (owner, id) = register_one(&env, &client, &admin);

let reason = Symbol::new(&env, "policy");
// Owner tries to use the admin-only suspend function — must panic
client.admin_suspend_restaurant(&owner, &id, &reason);
}

// -----------------------------------------------------------------------
// Legacy / other
// -----------------------------------------------------------------------

#[test]
#[should_panic(expected = "already initialized")]
fn test_double_init_panics() {
Expand Down
Loading