initial commit

This commit is contained in:
Timur Gordon
2025-06-27 04:13:31 +02:00
commit b2ee21999f
134 changed files with 35580 additions and 0 deletions

View File

@@ -0,0 +1,392 @@
use crate::models::*;
use gloo::storage::{LocalStorage, Storage};
use serde_json;
use std::collections::HashMap;
const COMPANIES_STORAGE_KEY: &str = "freezone_companies";
const REGISTRATION_FORM_KEY: &str = "freezone_registration_form";
const REGISTRATIONS_STORAGE_KEY: &str = "freezone_registrations";
const FORM_EXPIRY_HOURS: i64 = 24;
#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct CompanyRegistration {
pub id: u32,
pub company_name: String,
pub company_type: CompanyType,
pub status: RegistrationStatus,
pub created_at: String,
pub form_data: CompanyFormData,
pub current_step: u8,
}
#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum RegistrationStatus {
Draft,
PendingPayment,
PaymentFailed,
PendingApproval,
Approved,
Rejected,
}
impl RegistrationStatus {
pub fn to_string(&self) -> &'static str {
match self {
RegistrationStatus::Draft => "Draft",
RegistrationStatus::PendingPayment => "Pending Payment",
RegistrationStatus::PaymentFailed => "Payment Failed",
RegistrationStatus::PendingApproval => "Pending Approval",
RegistrationStatus::Approved => "Approved",
RegistrationStatus::Rejected => "Rejected",
}
}
pub fn get_badge_class(&self) -> &'static str {
match self {
RegistrationStatus::Draft => "bg-secondary",
RegistrationStatus::PendingPayment => "bg-warning",
RegistrationStatus::PaymentFailed => "bg-danger",
RegistrationStatus::PendingApproval => "bg-info",
RegistrationStatus::Approved => "bg-success",
RegistrationStatus::Rejected => "bg-danger",
}
}
}
pub struct CompanyService;
impl CompanyService {
/// Get all companies from local storage
pub fn get_companies() -> Vec<Company> {
match LocalStorage::get::<Vec<Company>>(COMPANIES_STORAGE_KEY) {
Ok(companies) => companies,
Err(_) => {
// Initialize with empty list if not found
let companies = Vec::new();
let _ = LocalStorage::set(COMPANIES_STORAGE_KEY, &companies);
companies
}
}
}
/// Save companies to local storage
pub fn save_companies(companies: &[Company]) -> Result<(), String> {
LocalStorage::set(COMPANIES_STORAGE_KEY, companies)
.map_err(|e| format!("Failed to save companies: {:?}", e))
}
/// Add a new company
pub fn add_company(mut company: Company) -> Result<Company, String> {
let mut companies = Self::get_companies();
// Generate new ID
let max_id = companies.iter().map(|c| c.id).max().unwrap_or(0);
company.id = max_id + 1;
// Generate registration number
company.registration_number = Self::generate_registration_number(&company.name);
companies.push(company.clone());
Self::save_companies(&companies)?;
Ok(company)
}
/// Update an existing company
pub fn update_company(updated_company: &Company) -> Result<(), String> {
let mut companies = Self::get_companies();
if let Some(company) = companies.iter_mut().find(|c| c.id == updated_company.id) {
*company = updated_company.clone();
Self::save_companies(&companies)?;
Ok(())
} else {
Err("Company not found".to_string())
}
}
/// Delete a company
pub fn delete_company(company_id: u32) -> Result<(), String> {
let mut companies = Self::get_companies();
companies.retain(|c| c.id != company_id);
Self::save_companies(&companies)
}
/// Get company by ID
pub fn get_company_by_id(company_id: u32) -> Option<Company> {
Self::get_companies().into_iter().find(|c| c.id == company_id)
}
/// Generate a registration number
fn generate_registration_number(company_name: &str) -> String {
let date = js_sys::Date::new_0();
let year = date.get_full_year();
let month = date.get_month() + 1; // JS months are 0-based
let day = date.get_date();
let prefix = company_name
.chars()
.take(3)
.collect::<String>()
.to_uppercase();
format!("FZC-{:04}{:02}{:02}-{}", year, month, day, prefix)
}
/// Save registration form data with expiration
pub fn save_registration_form(form_data: &CompanyFormData, current_step: u8) -> Result<(), String> {
let now = js_sys::Date::now() as i64;
let expires_at = now + (FORM_EXPIRY_HOURS * 60 * 60 * 1000);
let saved_form = SavedRegistrationForm {
form_data: form_data.clone(),
current_step,
saved_at: now,
expires_at,
};
LocalStorage::set(REGISTRATION_FORM_KEY, &saved_form)
.map_err(|e| format!("Failed to save form: {:?}", e))
}
/// Load registration form data if not expired
pub fn load_registration_form() -> Option<(CompanyFormData, u8)> {
match LocalStorage::get::<SavedRegistrationForm>(REGISTRATION_FORM_KEY) {
Ok(saved_form) => {
let now = js_sys::Date::now() as i64;
if now < saved_form.expires_at {
Some((saved_form.form_data, saved_form.current_step))
} else {
// Form expired, remove it
let _ = LocalStorage::delete(REGISTRATION_FORM_KEY);
None
}
}
Err(_) => None,
}
}
/// Clear saved registration form
pub fn clear_registration_form() -> Result<(), String> {
LocalStorage::delete(REGISTRATION_FORM_KEY);
Ok(())
}
/// Validate form data for a specific step
pub fn validate_step(form_data: &CompanyFormData, step: u8) -> ValidationResult {
let mut errors = Vec::new();
match step {
1 => {
if form_data.company_name.trim().is_empty() {
errors.push("Company name is required".to_string());
} else if form_data.company_name.len() < 2 {
errors.push("Company name must be at least 2 characters".to_string());
}
if form_data.company_email.trim().is_empty() {
errors.push("Company email is required".to_string());
} else if !Self::is_valid_email(&form_data.company_email) {
errors.push("Please enter a valid email address".to_string());
}
}
2 => {
// Company type is always valid since it's a dropdown
}
3 => {
if form_data.shareholders.is_empty() {
errors.push("At least one shareholder is required".to_string());
} else {
let total_percentage: f64 = form_data.shareholders.iter().map(|s| s.percentage).sum();
if (total_percentage - 100.0).abs() > 0.01 {
errors.push(format!("Shareholder percentages must add up to 100% (currently {:.1}%)", total_percentage));
}
for (i, shareholder) in form_data.shareholders.iter().enumerate() {
if shareholder.name.trim().is_empty() {
errors.push(format!("Shareholder {} name is required", i + 1));
}
if shareholder.percentage <= 0.0 || shareholder.percentage > 100.0 {
errors.push(format!("Shareholder {} percentage must be between 0 and 100", i + 1));
}
}
}
}
4 => {
if !form_data.legal_agreements.all_agreed() {
let missing = form_data.legal_agreements.missing_agreements();
errors.push(format!("Please accept all required agreements: {}", missing.join(", ")));
}
}
_ => {
errors.push("Invalid step".to_string());
}
}
if errors.is_empty() {
ValidationResult::valid()
} else {
ValidationResult::invalid(errors)
}
}
/// Simple email validation
fn is_valid_email(email: &str) -> bool {
email.contains('@') && email.contains('.') && email.len() > 5
}
/// Create a company from form data (simulated)
pub fn create_company_from_form(form_data: &CompanyFormData) -> Result<Company, String> {
let now = js_sys::Date::new_0();
let incorporation_date = format!(
"{:04}-{:02}-{:02}",
now.get_full_year(),
now.get_month() + 1,
now.get_date()
);
let company = Company {
id: 0, // Will be set by add_company
name: form_data.company_name.clone(),
company_type: form_data.company_type.clone(),
status: CompanyStatus::PendingPayment,
registration_number: String::new(), // Will be generated by add_company
incorporation_date,
email: Some(form_data.company_email.clone()),
phone: Some(form_data.company_phone.clone()),
website: form_data.company_website.clone(),
address: Some(form_data.company_address.clone()),
industry: form_data.company_industry.clone(),
description: form_data.company_purpose.clone(),
fiscal_year_end: form_data.fiscal_year_end.clone(),
shareholders: form_data.shareholders.clone(),
};
Self::add_company(company)
}
/// Calculate total payment amount
pub fn calculate_payment_amount(company_type: &CompanyType, payment_plan: &PaymentPlan) -> f64 {
let pricing = company_type.get_pricing();
let twin_fee = 2.0; // ZDFZ Twin fee
let monthly_total = pricing.monthly_fee + twin_fee;
let subscription_amount = match payment_plan {
PaymentPlan::Monthly => monthly_total,
PaymentPlan::Yearly => monthly_total * 12.0 * payment_plan.get_discount(),
PaymentPlan::TwoYear => monthly_total * 24.0 * payment_plan.get_discount(),
};
pricing.setup_fee + subscription_amount
}
/// Initialize with sample data for demonstration
pub fn initialize_sample_data() -> Result<(), String> {
let companies = Self::get_companies();
if companies.is_empty() {
let sample_companies = vec![
Company {
id: 1,
name: "Zanzibar Digital Solutions".to_string(),
company_type: CompanyType::StartupFZC,
status: CompanyStatus::Active,
registration_number: "FZC-20250101-ZAN".to_string(),
incorporation_date: "2025-01-01".to_string(),
email: Some("contact@zanzibar-digital.com".to_string()),
phone: Some("+255 123 456 789".to_string()),
website: Some("https://zanzibar-digital.com".to_string()),
address: Some("Stone Town, Zanzibar".to_string()),
industry: Some("Technology".to_string()),
description: Some("Digital solutions and blockchain development".to_string()),
fiscal_year_end: Some("12-31".to_string()),
shareholders: vec![
Shareholder {
name: "John Smith".to_string(),
resident_id: "ID123456789".to_string(),
percentage: 60.0,
},
Shareholder {
name: "Sarah Johnson".to_string(),
resident_id: "ID987654321".to_string(),
percentage: 40.0,
},
],
},
Company {
id: 2,
name: "Ocean Trading Co".to_string(),
company_type: CompanyType::GrowthFZC,
status: CompanyStatus::Active,
registration_number: "FZC-20250102-OCE".to_string(),
incorporation_date: "2025-01-02".to_string(),
email: Some("info@ocean-trading.com".to_string()),
phone: Some("+255 987 654 321".to_string()),
website: None,
address: Some("Pemba Island, Zanzibar".to_string()),
industry: Some("Trading".to_string()),
description: Some("International trading and logistics".to_string()),
fiscal_year_end: Some("06-30".to_string()),
shareholders: vec![
Shareholder {
name: "Ahmed Hassan".to_string(),
resident_id: "ID555666777".to_string(),
percentage: 100.0,
},
],
},
];
Self::save_companies(&sample_companies)?;
}
Ok(())
}
/// Get all registrations from local storage
pub fn get_registrations() -> Vec<CompanyRegistration> {
match LocalStorage::get::<Vec<CompanyRegistration>>(REGISTRATIONS_STORAGE_KEY) {
Ok(registrations) => registrations,
Err(_) => {
// Initialize with empty list if not found
let registrations = Vec::new();
let _ = LocalStorage::set(REGISTRATIONS_STORAGE_KEY, &registrations);
registrations
}
}
}
/// Save registrations to local storage
pub fn save_registrations(registrations: &[CompanyRegistration]) -> Result<(), String> {
LocalStorage::set(REGISTRATIONS_STORAGE_KEY, registrations)
.map_err(|e| format!("Failed to save registrations: {:?}", e))
}
/// Add or update a registration
pub fn save_registration(mut registration: CompanyRegistration) -> Result<CompanyRegistration, String> {
let mut registrations = Self::get_registrations();
if registration.id == 0 {
// Generate new ID for new registration
let max_id = registrations.iter().map(|r| r.id).max().unwrap_or(0);
registration.id = max_id + 1;
registrations.push(registration.clone());
} else {
// Update existing registration
if let Some(existing) = registrations.iter_mut().find(|r| r.id == registration.id) {
*existing = registration.clone();
} else {
return Err("Registration not found".to_string());
}
}
Self::save_registrations(&registrations)?;
Ok(registration)
}
}
#[derive(Clone, serde::Serialize, serde::Deserialize)]
struct SavedRegistrationForm {
form_data: CompanyFormData,
current_step: u8,
saved_at: i64,
expires_at: i64,
}

View File

@@ -0,0 +1,223 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Plan {
pub id: String,
pub name: String,
pub price: f64,
pub features: Vec<String>,
pub popular: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaymentMethod {
pub id: String,
pub method_type: String,
pub last_four: String,
pub expires: Option<String>,
pub is_primary: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Invoice {
pub id: String,
pub date: String,
pub description: String,
pub amount: f64,
pub status: String,
pub pdf_url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Subscription {
pub plan: Plan,
pub next_billing_date: String,
pub status: String,
}
#[derive(Debug, Clone)]
pub struct MockBillingApi {
pub current_subscription: Subscription,
pub available_plans: Vec<Plan>,
pub payment_methods: Vec<PaymentMethod>,
pub invoices: Vec<Invoice>,
}
impl Default for MockBillingApi {
fn default() -> Self {
Self::new()
}
}
impl MockBillingApi {
pub fn new() -> Self {
let available_plans = vec![
Plan {
id: "starter".to_string(),
name: "Starter".to_string(),
price: 29.0,
features: vec![
"Up to 100 transactions".to_string(),
"Basic reporting".to_string(),
"Email support".to_string(),
],
popular: false,
},
Plan {
id: "business_pro".to_string(),
name: "Business Pro".to_string(),
price: 99.0,
features: vec![
"Unlimited transactions".to_string(),
"Advanced reporting".to_string(),
"Priority support".to_string(),
"API access".to_string(),
],
popular: true,
},
Plan {
id: "enterprise".to_string(),
name: "Enterprise".to_string(),
price: 299.0,
features: vec![
"Unlimited everything".to_string(),
"Custom integrations".to_string(),
"Dedicated support".to_string(),
"SLA guarantee".to_string(),
"White-label options".to_string(),
],
popular: false,
},
];
let current_subscription = Subscription {
plan: available_plans[1].clone(), // Business Pro
next_billing_date: "January 15, 2025".to_string(),
status: "active".to_string(),
};
let payment_methods = vec![
PaymentMethod {
id: "card_4242".to_string(),
method_type: "Credit Card".to_string(),
last_four: "4242".to_string(),
expires: Some("12/26".to_string()),
is_primary: true,
},
PaymentMethod {
id: "bank_5678".to_string(),
method_type: "Bank Transfer".to_string(),
last_four: "5678".to_string(),
expires: None,
is_primary: false,
},
];
let invoices = vec![
Invoice {
id: "inv_001".to_string(),
date: "Dec 15, 2024".to_string(),
description: "Business Pro - Monthly".to_string(),
amount: 99.0,
status: "Paid".to_string(),
pdf_url: "data:application/pdf;base64,JVBERi0xLjQKJdPr6eEKMSAwIG9iago8PAovVGl0bGUgKEludm9pY2UgIzAwMSkKL0NyZWF0b3IgKE1vY2sgQmlsbGluZyBBUEkpCi9Qcm9kdWNlciAoTW9jayBCaWxsaW5nIEFQSSkKL0NyZWF0aW9uRGF0ZSAoRDoyMDI0MTIxNTAwMDAwMFopCj4+CmVuZG9iagoyIDAgb2JqCjw8Ci9UeXBlIC9DYXRhbG9nCi9QYWdlcyAzIDAgUgo+PgplbmRvYmoKMyAwIG9iago8PAovVHlwZSAvUGFnZXMKL0tpZHMgWzQgMCBSXQovQ291bnQgMQo+PgplbmRvYmoKNCAwIG9iago8PAovVHlwZSAvUGFnZQovUGFyZW50IDMgMCBSCi9NZWRpYUJveCBbMCAwIDYxMiA3OTJdCi9Db250ZW50cyA1IDAgUgo+PgplbmRvYmoKNSAwIG9iago8PAovTGVuZ3RoIDQ0Cj4+CnN0cmVhbQpCVApxCjcyIDcyMCA3MiA3MjAgcmUKUwpRCkJUCi9GMSAxMiBUZgo3MiA3MDAgVGQKKEludm9pY2UgIzAwMSkgVGoKRVQKZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqCjw8Ci9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMQovQmFzZUZvbnQgL0hlbHZldGljYQo+PgplbmRvYmoKeHJlZgowIDcKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAxNzQgMDAwMDAgbiAKMDAwMDAwMDIyMSAwMDAwMCBuIAowMDAwMDAwMjc4IDAwMDAwIG4gCjAwMDAwMDAzNzUgMDAwMDAgbiAKMDAwMDAwMDQ2OSAwMDAwMCBuIAp0cmFpbGVyCjw8Ci9TaXplIDcKL1Jvb3QgMiAwIFIKPj4Kc3RhcnR4cmVmCjU2NwolJUVPRgo=".to_string(),
},
Invoice {
id: "inv_002".to_string(),
date: "Nov 15, 2024".to_string(),
description: "Business Pro - Monthly".to_string(),
amount: 99.0,
status: "Paid".to_string(),
pdf_url: "data:application/pdf;base64,JVBERi0xLjQKJdPr6eEKMSAwIG9iago8PAovVGl0bGUgKEludm9pY2UgIzAwMikKL0NyZWF0b3IgKE1vY2sgQmlsbGluZyBBUEkpCi9Qcm9kdWNlciAoTW9jayBCaWxsaW5nIEFQSSkKL0NyZWF0aW9uRGF0ZSAoRDoyMDI0MTExNTAwMDAwMFopCj4+CmVuZG9iagoyIDAgb2JqCjw8Ci9UeXBlIC9DYXRhbG9nCi9QYWdlcyAzIDAgUgo+PgplbmRvYmoKMyAwIG9iago8PAovVHlwZSAvUGFnZXMKL0tpZHMgWzQgMCBSXQovQ291bnQgMQo+PgplbmRvYmoKNCAwIG9iago8PAovVHlwZSAvUGFnZQovUGFyZW50IDMgMCBSCi9NZWRpYUJveCBbMCAwIDYxMiA3OTJdCi9Db250ZW50cyA1IDAgUgo+PgplbmRvYmoKNSAwIG9iago8PAovTGVuZ3RoIDQ0Cj4+CnN0cmVhbQpCVApxCjcyIDcyMCA3MiA3MjAgcmUKUwpRCkJUCi9GMSAxMiBUZgo3MiA3MDAgVGQKKEludm9pY2UgIzAwMikgVGoKRVQKZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqCjw8Ci9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMQovQmFzZUZvbnQgL0hlbHZldGljYQo+PgplbmRvYmoKeHJlZgowIDcKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAxNzQgMDAwMDAgbiAKMDAwMDAwMDIyMSAwMDAwMCBuIAowMDAwMDAwMjc4IDAwMDAwIG4gCjAwMDAwMDAzNzUgMDAwMDAgbiAKMDAwMDAwMDQ2OSAwMDAwMCBuIAp0cmFpbGVyCjw8Ci9TaXplIDcKL1Jvb3QgMiAwIFIKPj4Kc3RhcnR4cmVmCjU2NwolJUVPRgo=".to_string(),
},
Invoice {
id: "inv_003".to_string(),
date: "Oct 15, 2024".to_string(),
description: "Business Pro - Monthly".to_string(),
amount: 99.0,
status: "Paid".to_string(),
pdf_url: "data:application/pdf;base64,JVBERi0xLjQKJdPr6eEKMSAwIG9iago8PAovVGl0bGUgKEludm9pY2UgIzAwMykKL0NyZWF0b3IgKE1vY2sgQmlsbGluZyBBUEkpCi9Qcm9kdWNlciAoTW9jayBCaWxsaW5nIEFQSSkKL0NyZWF0aW9uRGF0ZSAoRDoyMDI0MTAxNTAwMDAwMFopCj4+CmVuZG9iagoyIDAgb2JqCjw8Ci9UeXBlIC9DYXRhbG9nCi9QYWdlcyAzIDAgUgo+PgplbmRvYmoKMyAwIG9iago8PAovVHlwZSAvUGFnZXMKL0tpZHMgWzQgMCBSXQovQ291bnQgMQo+PgplbmRvYmoKNCAwIG9iago8PAovVHlwZSAvUGFnZQovUGFyZW50IDMgMCBSCi9NZWRpYUJveCBbMCAwIDYxMiA3OTJdCi9Db250ZW50cyA1IDAgUgo+PgplbmRvYmoKNSAwIG9iago8PAovTGVuZ3RoIDQ0Cj4+CnN0cmVhbQpCVApxCjcyIDcyMCA3MiA3MjAgcmUKUwpRCkJUCi9GMSAxMiBUZgo3MiA3MDAgVGQKKEludm9pY2UgIzAwMykgVGoKRVQKZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqCjw8Ci9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMQovQmFzZUZvbnQgL0hlbHZldGljYQo+PgplbmRvYmoKeHJlZgowIDcKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAxNzQgMDAwMDAgbiAKMDAwMDAwMDIyMSAwMDAwMCBuIAowMDAwMDAwMjc4IDAwMDAwIG4gCjAwMDAwMDAzNzUgMDAwMDAgbiAKMDAwMDAwMDQ2OSAwMDAwMCBuIAp0cmFpbGVyCjw8Ci9TaXplIDcKL1Jvb3QgMiAwIFIKPj4Kc3RhcnR4cmVmCjU2NwolJUVPRgo=".to_string(),
},
Invoice {
id: "inv_004".to_string(),
date: "Sep 15, 2024".to_string(),
description: "Setup Fee".to_string(),
amount: 50.0,
status: "Paid".to_string(),
pdf_url: "data:application/pdf;base64,JVBERi0xLjQKJdPr6eEKMSAwIG9iago8PAovVGl0bGUgKFNldHVwIEZlZSBJbnZvaWNlKQovQ3JlYXRvciAoTW9jayBCaWxsaW5nIEFQSSkKL1Byb2R1Y2VyIChNb2NrIEJpbGxpbmcgQVBJKQovQ3JlYXRpb25EYXRlIChEOjIwMjQwOTE1MDAwMDAwWikKPj4KZW5kb2JqCjIgMCBvYmoKPDwKL1R5cGUgL0NhdGFsb2cKL1BhZ2VzIDMgMCBSCj4+CmVuZG9iagozIDAgb2JqCjw8Ci9UeXBlIC9QYWdlcwovS2lkcyBbNCAwIFJdCi9Db3VudCAxCj4+CmVuZG9iago0IDAgb2JqCjw8Ci9UeXBlIC9QYWdlCi9QYXJlbnQgMyAwIFIKL01lZGlhQm94IFswIDAgNjEyIDc5Ml0KL0NvbnRlbnRzIDUgMCBSCj4+CmVuZG9iago1IDAgb2JqCjw8Ci9MZW5ndGggNDQKPj4Kc3RyZWFtCkJUCnEKNzIgNzIwIDcyIDcyMCByZQpTClEKQlQKL0YxIDEyIFRmCjcyIDcwMCBUZAooU2V0dXAgRmVlKSBUagpFVAplbmRzdHJlYW0KZW5kb2JqCjYgMCBvYmoKPDwKL1R5cGUgL0ZvbnQKL1N1YnR5cGUgL1R5cGUxCi9CYXNlRm9udCAvSGVsdmV0aWNhCj4+CmVuZG9iagp4cmVmCjAgNwowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMDkgMDAwMDAgbiAKMDAwMDAwMDE3NCAwMDAwMCBuIAowMDAwMDAwMjIxIDAwMDAwIG4gCjAwMDAwMDAyNzggMDAwMDAgbiAKMDAwMDAwMDM3NSAwMDAwMCBuIAowMDAwMDAwNDY5IDAwMDAwIG4gCnRyYWlsZXIKPDwKL1NpemUgNwovUm9vdCAyIDAgUgo+PgpzdGFydHhyZWYKNTY3CiUlRU9GCg==".to_string(),
},
];
Self {
current_subscription,
available_plans,
payment_methods,
invoices,
}
}
// Subscription methods
pub async fn get_current_subscription(&self) -> Result<Subscription, String> {
// Simulate API delay
Ok(self.current_subscription.clone())
}
pub async fn get_available_plans(&self) -> Result<Vec<Plan>, String> {
Ok(self.available_plans.clone())
}
pub async fn change_plan(&mut self, plan_id: &str) -> Result<Subscription, String> {
if let Some(plan) = self.available_plans.iter().find(|p| p.id == plan_id) {
self.current_subscription.plan = plan.clone();
Ok(self.current_subscription.clone())
} else {
Err("Plan not found".to_string())
}
}
pub async fn cancel_subscription(&mut self) -> Result<String, String> {
self.current_subscription.status = "cancelled".to_string();
Ok("Subscription cancelled successfully".to_string())
}
// Payment methods
pub async fn get_payment_methods(&self) -> Result<Vec<PaymentMethod>, String> {
Ok(self.payment_methods.clone())
}
pub async fn add_payment_method(&mut self, method_type: &str, last_four: &str) -> Result<PaymentMethod, String> {
let new_method = PaymentMethod {
id: format!("{}_{}", method_type.to_lowercase(), last_four),
method_type: method_type.to_string(),
last_four: last_four.to_string(),
expires: if method_type == "Credit Card" { Some("12/28".to_string()) } else { None },
is_primary: false,
};
self.payment_methods.push(new_method.clone());
Ok(new_method)
}
pub async fn remove_payment_method(&mut self, method_id: &str) -> Result<String, String> {
if let Some(pos) = self.payment_methods.iter().position(|m| m.id == method_id) {
self.payment_methods.remove(pos);
Ok("Payment method removed successfully".to_string())
} else {
Err("Payment method not found".to_string())
}
}
// Invoices
pub async fn get_invoices(&self) -> Result<Vec<Invoice>, String> {
Ok(self.invoices.clone())
}
pub async fn download_invoice(&self, invoice_id: &str) -> Result<String, String> {
if let Some(invoice) = self.invoices.iter().find(|i| i.id == invoice_id) {
Ok(invoice.pdf_url.clone())
} else {
Err("Invoice not found".to_string())
}
}
}

View File

@@ -0,0 +1,7 @@
pub mod mock_billing_api;
pub mod company_service;
pub mod resident_service;
pub use mock_billing_api::*;
pub use company_service::*;
pub use resident_service::*;

View File

@@ -0,0 +1,257 @@
use crate::models::company::{DigitalResident, DigitalResidentFormData, KycStatus};
use gloo::storage::{LocalStorage, Storage};
const RESIDENTS_STORAGE_KEY: &str = "freezone_residents";
const RESIDENT_REGISTRATIONS_STORAGE_KEY: &str = "freezone_resident_registrations";
const RESIDENT_FORM_KEY: &str = "freezone_resident_registration_form";
const FORM_EXPIRY_HOURS: i64 = 24;
#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ResidentRegistration {
pub id: u32,
pub full_name: String,
pub email: String,
pub status: ResidentRegistrationStatus,
pub created_at: String,
pub form_data: DigitalResidentFormData,
pub current_step: u8,
}
#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum ResidentRegistrationStatus {
Draft,
PendingPayment,
PaymentFailed,
PendingApproval,
Approved,
Rejected,
}
impl ResidentRegistrationStatus {
pub fn to_string(&self) -> &'static str {
match self {
ResidentRegistrationStatus::Draft => "Draft",
ResidentRegistrationStatus::PendingPayment => "Pending Payment",
ResidentRegistrationStatus::PaymentFailed => "Payment Failed",
ResidentRegistrationStatus::PendingApproval => "Pending Approval",
ResidentRegistrationStatus::Approved => "Approved",
ResidentRegistrationStatus::Rejected => "Rejected",
}
}
pub fn get_badge_class(&self) -> &'static str {
match self {
ResidentRegistrationStatus::Draft => "bg-secondary",
ResidentRegistrationStatus::PendingPayment => "bg-warning",
ResidentRegistrationStatus::PaymentFailed => "bg-danger",
ResidentRegistrationStatus::PendingApproval => "bg-info",
ResidentRegistrationStatus::Approved => "bg-success",
ResidentRegistrationStatus::Rejected => "bg-danger",
}
}
}
pub struct ResidentService;
impl ResidentService {
/// Get all residents from local storage
pub fn get_residents() -> Vec<DigitalResident> {
match LocalStorage::get::<Vec<DigitalResident>>(RESIDENTS_STORAGE_KEY) {
Ok(residents) => residents,
Err(_) => {
// Initialize with empty list if not found
let residents = Vec::new();
let _ = LocalStorage::set(RESIDENTS_STORAGE_KEY, &residents);
residents
}
}
}
/// Save residents to local storage
pub fn save_residents(residents: &[DigitalResident]) -> Result<(), String> {
LocalStorage::set(RESIDENTS_STORAGE_KEY, residents)
.map_err(|e| format!("Failed to save residents: {:?}", e))
}
/// Add a new resident
pub fn add_resident(mut resident: DigitalResident) -> Result<DigitalResident, String> {
let mut residents = Self::get_residents();
// Generate new ID
let max_id = residents.iter().map(|r| r.id).max().unwrap_or(0);
resident.id = max_id + 1;
residents.push(resident.clone());
Self::save_residents(&residents)?;
Ok(resident)
}
/// Get all resident registrations from local storage
pub fn get_resident_registrations() -> Vec<ResidentRegistration> {
match LocalStorage::get::<Vec<ResidentRegistration>>(RESIDENT_REGISTRATIONS_STORAGE_KEY) {
Ok(registrations) => registrations,
Err(_) => {
// Initialize with empty list if not found
let registrations = Vec::new();
let _ = LocalStorage::set(RESIDENT_REGISTRATIONS_STORAGE_KEY, &registrations);
registrations
}
}
}
/// Save resident registrations to local storage
pub fn save_resident_registrations(registrations: &[ResidentRegistration]) -> Result<(), String> {
LocalStorage::set(RESIDENT_REGISTRATIONS_STORAGE_KEY, registrations)
.map_err(|e| format!("Failed to save resident registrations: {:?}", e))
}
/// Add or update a resident registration
pub fn save_resident_registration(mut registration: ResidentRegistration) -> Result<ResidentRegistration, String> {
let mut registrations = Self::get_resident_registrations();
if registration.id == 0 {
// Generate new ID for new registration
let max_id = registrations.iter().map(|r| r.id).max().unwrap_or(0);
registration.id = max_id + 1;
registrations.push(registration.clone());
} else {
// Update existing registration
if let Some(existing) = registrations.iter_mut().find(|r| r.id == registration.id) {
*existing = registration.clone();
} else {
return Err("Registration not found".to_string());
}
}
Self::save_resident_registrations(&registrations)?;
Ok(registration)
}
/// Save registration form data with expiration
pub fn save_resident_registration_form(form_data: &DigitalResidentFormData, current_step: u8) -> Result<(), String> {
let now = js_sys::Date::now() as i64;
let expires_at = now + (FORM_EXPIRY_HOURS * 60 * 60 * 1000);
let saved_form = SavedResidentRegistrationForm {
form_data: form_data.clone(),
current_step,
saved_at: now,
expires_at,
};
LocalStorage::set(RESIDENT_FORM_KEY, &saved_form)
.map_err(|e| format!("Failed to save form: {:?}", e))
}
/// Load registration form data if not expired
pub fn load_resident_registration_form() -> Option<(DigitalResidentFormData, u8)> {
match LocalStorage::get::<SavedResidentRegistrationForm>(RESIDENT_FORM_KEY) {
Ok(saved_form) => {
let now = js_sys::Date::now() as i64;
if now < saved_form.expires_at {
Some((saved_form.form_data, saved_form.current_step))
} else {
// Form expired, remove it
let _ = LocalStorage::delete(RESIDENT_FORM_KEY);
None
}
}
Err(_) => None,
}
}
/// Clear saved registration form
pub fn clear_resident_registration_form() -> Result<(), String> {
LocalStorage::delete(RESIDENT_FORM_KEY);
Ok(())
}
/// Create a resident from form data
pub fn create_resident_from_form(form_data: &DigitalResidentFormData) -> Result<DigitalResident, String> {
let now = js_sys::Date::new_0();
let registration_date = format!(
"{:04}-{:02}-{:02}",
now.get_full_year(),
now.get_month() + 1,
now.get_date()
);
let resident = DigitalResident {
id: 0, // Will be set by add_resident
full_name: form_data.full_name.clone(),
email: form_data.email.clone(),
phone: form_data.phone.clone(),
date_of_birth: form_data.date_of_birth.clone(),
nationality: form_data.nationality.clone(),
passport_number: form_data.passport_number.clone(),
passport_expiry: form_data.passport_expiry.clone(),
current_address: form_data.current_address.clone(),
city: form_data.city.clone(),
country: form_data.country.clone(),
postal_code: form_data.postal_code.clone(),
occupation: form_data.occupation.clone(),
employer: form_data.employer.clone(),
annual_income: form_data.annual_income.clone(),
education_level: form_data.education_level.clone(),
selected_services: form_data.requested_services.clone(),
payment_plan: form_data.payment_plan.clone(),
registration_date,
status: crate::models::company::ResidentStatus::Pending,
kyc_documents_uploaded: false, // Will be updated when documents are uploaded
kyc_status: KycStatus::NotStarted,
public_key: form_data.public_key.clone(),
};
Self::add_resident(resident)
}
/// Validate form data for a specific step (simplified 2-step form)
pub fn validate_resident_step(form_data: &DigitalResidentFormData, step: u8) -> crate::models::ValidationResult {
let mut errors = Vec::new();
match step {
1 => {
// Step 1: Personal Information & KYC (simplified - only name, email, and terms required)
if form_data.full_name.trim().is_empty() {
errors.push("Full name is required".to_string());
}
if form_data.email.trim().is_empty() {
errors.push("Email is required".to_string());
} else if !Self::is_valid_email(&form_data.email) {
errors.push("Please enter a valid email address".to_string());
}
if !form_data.legal_agreements.terms {
errors.push("You must agree to the Terms of Service and Privacy Policy".to_string());
}
// Note: KYC verification is handled separately via button click
}
2 => {
// Step 2: Payment only (no additional agreements needed)
// Payment validation will be handled by Stripe
}
_ => {
errors.push("Invalid step".to_string());
}
}
if errors.is_empty() {
crate::models::ValidationResult { is_valid: true, errors: Vec::new() }
} else {
crate::models::ValidationResult { is_valid: false, errors }
}
}
/// Simple email validation
fn is_valid_email(email: &str) -> bool {
email.contains('@') && email.contains('.') && email.len() > 5
}
}
#[derive(Clone, serde::Serialize, serde::Deserialize)]
struct SavedResidentRegistrationForm {
form_data: DigitalResidentFormData,
current_step: u8,
saved_at: i64,
expires_at: i64,
}