# OSchema for freezone/main # ═══════════════════════════════════════════════════════════════════════════════ # OSIS Schema Definition # ═══════════════════════════════════════════════════════════════════════════════ # # RPC Endpoint: POST /{context}/{domain}/rpc # Content-Type: application/json # # Request format (JSON-RPC 2.0): # {"jsonrpc": "2.0", "method": "TypeName.method", "params": {...}, "id": 1} # # Available CRUD methods for root objects: # - TypeName.new -> Create new object (returns empty template) # - TypeName.get -> Get by sid: {"sid": "..."} # - TypeName.set -> Create/update: {"data": ""} # - TypeName.delete -> Delete by sid: {"sid": "..."} # - TypeName.list -> List all sids # - TypeName.find -> Search: {"query": "..."} # # ═══════════════════════════════════════════════════════════════════════════════ # ===SCHEMA=== # ─────────────────────────────────────────────────────────────────────────────── # ROOT OBJECTS - Storable entities with CRUD operations # ─────────────────────────────────────────────────────────────────────────────── # Admin audit log entry [rootobject] AdminAction = { sid: str # SmartID for storage admin_email: str @index # Admin who performed the action action: str @index # Action type (e.g., "suspend_user", "approve_kyc") target_type: str # Entity type affected (user, resident, company, payment) target_id: str @index # ID of affected entity details: str # JSON details of the action created_at: otime # When the action was performed } # API key for reseller access [rootobject] ApiKey = { sid: str # SmartID for storage key_hash: str @index # Hashed API key (SHA-256 hex) name: str @index # Key name/description user_id?: str # Optional user reference is_active: bool # Key active status last_used_at?: otime # Last usage timestamp created_at: otime expires_at?: otime # Optional expiration discount_profile?: str # Discount profile name (e.g. "tftc"), maps to [discounts.] in branding.toml allow_company_creation: bool # Whether this key can create companies (default false) mcp_scope?: str # MCP access scope: "read" or "readwrite" (default: none = no MCP access) max_transaction_amount?: f64 # Max amount per MCP transaction (default 100.0, 0 = unlimited) # Tier 2: Aggregate spending limits daily_spending_limit?: f64 # Max EUR per calendar day (default 500.0, 0 = unlimited) monthly_spending_limit?: f64 # Max EUR per calendar month (default 2000.0, 0 = unlimited) # Tier 3: Rate limiting max_calls_per_hour?: i32 # Max MCP calls per hour (default 60, 0 = unlimited) max_calls_per_day?: i32 # Max MCP calls per calendar day (default 500, 0 = unlimited) # Tier 4: Granular tool permissions allowed_tool_patterns?: str # Comma-separated glob patterns for allowed tools (empty = all allowed) blocked_tool_patterns?: str # Comma-separated glob patterns for blocked tools (checked first) # Tier 6: Approval queue approval_threshold?: f64 # Amount above which approval is required (0 = off) approval_required_tools?: str # Comma-separated tool patterns that always need approval # Partner track (D-018, s60): identifies which partner owns this reseller key. # Non-unique — a partner may rotate keys during a cutover window. Lookup via [partner_config.] in branding.toml. partner_id?: str @index # Partner identifier (e.g. "tftc") — None for personal keys and pre-s60 reseller keys # Auto-minted by the platform on resident creation to power the AI chat proxy. # Hidden from `list_personal` so users don't see a system row in their Profile API Keys # panel and accidentally revoke the key the AI proxy depends on. is_system_minted: bool # True for keys auto-created by the platform; user-minted keys are false } # ─────────────────────────────────────────────────────────────────────────────── # BUSINESS DOCUMENTS # ─────────────────────────────────────────────────────────────────────────────── # Uploaded or generated business document [rootobject] BusinessDocument = { sid: str # SmartID for storage (doc_id) company_id: str @index # Reference to FreeZoneCompany document_type: BusinessDocumentType # Certificate, License, Memorandum, etc. name: str @index # Document name description?: str # Document description file_url?: str # URL to file content?: str # Content for generated docs is_official: bool # Whether issued by FreeZone issued_date?: otime # When document was issued expiry_date?: otime # When document expires uploaded_by?: str # Resident ID who uploaded created_at: otime # Creation timestamp updated_at: otime # Last update timestamp } # Company message entity [rootobject] CompanyMessage = { sid: str # SmartID for storage company_id: str @index # Reference to FreeZoneCompany title: str @index # Message title body: str @index # Message body/content message_type: MessageType # Type (system/notification/alert) category?: MessageCategory # Category action_url?: str # URL for action button action_label?: str # Label for action button is_read: bool # Read status created_at: otime # Creation timestamp updated_at: otime # Last update timestamp } # CompanyRole — role + time + classification, the primitive from FREEZONE.md principle 2. # One row per (subject × role × company × validity interval). Append-only (principle 3): # supersede by closing valid_to and writing a new row, never edit in place. # # subject_id is a formatted ID string, prefix-discriminated: # "ZDFZ-YYYY-RA..." → DigitalResident # "ZDFZ-C-YYYY-..." → CorporateShareholder (M2 forward-compat per e2e §X M1-ROLE-12) # company_id is the formatted company/coop ID (ZDFZ-YYYY-Y... for FZC, ...-P... for coop). # classification_scope enumerates the audience tiers for which this role is visible when # DocumentAudience or visibility filters compute (current_roles × classification) in a later session. # [rootobject] CompanyRole = { sid: str # SmartID for storage company_id: str @index # Formatted company ID (ZDFZ-YYYY-Y/P...) subject_id: str @index # Formatted subject ID; prefix discriminates variant role: CompanyRoleKind @index # shareholder / founding_member / platinum_member / founder valid_from: otime @index # Inclusive — role is held from this instant valid_to?: otime # Exclusive — role ends at this instant (None = still current) classification_scope: [str] # Audience tiers (e.g. ["public","shareholder","founder"]) created_at: otime created_by: str # Formatted ID of the actor who wrote this row holder_name?: text # home#589: display-only denorm — role-holder's full name, resolved from subject_id at read time so the FE renders a person, not the sequential subject_id. None = subject not resolvable (corporate/legacy). Never feeds authz. holder_external_id?: text # home#589/#591: display-only denorm — role-holder's opaque external_id (FPE id), resolved from subject_id at read time. The ONLY id the FE may render or self-join on for a holder; user-facing list output blanks subject_id when this is present. } # Business Contract entity [rootobject] Contract = { sid: str # SmartID for storage fzc_id: str @index # Reference to FreeZoneCompany name: str @index # Contract name contract_type: str @index # Contract type counterparty: str @index # Other party name status: ContractStatus # Contract status start_date: otime # Start date end_date?: otime # End date total_value?: f64 # Contract value created_at: otime # Creation timestamp updated_at: otime # Last update timestamp } # Contract Signature entity [rootobject] ContractSignature = { sid: str # SmartID for storage (signature_id) user_id: str @index # Reference to User contract_id: str @index # Reference to RegistrationContract contract_version: str # Version that was signed receipt_id: str # Unique signing receipt identifier (non-guessable) signed_at: otime # Signature timestamp entity_type: str # "user", "digital_resident", "company" entity_name: str # Full name of signing entity entity_id: str # Empty for user, DR SID, Company SID ip_address?: str # IP address at signing created_at: otime # Creation timestamp } # Contract Template entity [rootobject] ContractTemplate = { sid: str # SmartID for storage name: str @index # Template name description: str @index # Template description category: str @index # Template category content: str # Template content created_at: otime # Creation timestamp } # Cooperative calendar event (D-104 Stage-1). Minimal meeting/vote/other record owned # by a coop. Created by a director (director-gated entitydraftservice.create_event), # readable by any member (entitydraftservice.list_events). Accessed ONLY via those gated # service methods — deliberately no generic coopevent.* CRUD RPC (that would bypass the # director gate). Reads use coopevent_list() + filter (the CompanyRole pattern). CoopEvent = { sid: str company_id: str @index # Formatted coop ID this event belongs to title: str description?: str event_type: str # "meeting" | "vote_deadline" | "other" start_at: otime end_at?: otime created_by: str @index # Formatted resident_id of the creating director created_at: otime } # Credit balance per user [rootobject] CreditBalance = { sid: str # SmartID for storage user_id: str @index # Reference to User (one balance per user) balance: f64 # Current credit balance (USD) lifetime_earned: f64 # Total credits ever earned/purchased lifetime_spent: f64 # Total credits ever spent last_top_up_at?: otime # When last top-up occurred created_at: otime # Creation timestamp updated_at: otime # Last update timestamp # Auto top-up settings auto_topup_enabled: bool # Whether auto top-up is active auto_topup_amount: f64 # Amount to top up auto_topup_threshold: f64 # Trigger when balance drops below this stripe_customer_id?: str # Stripe customer ID for auto top-up stripe_payment_method_id?: str # Stripe payment method for auto top-up } # Credit transaction ledger entry [rootobject] CreditTransaction = { sid: str # SmartID for storage user_id: str @index # Reference to User tx_type: CreditTransactionType # top_up, deduction, refund, bonus, expiry amount: f64 # Transaction amount (always positive) balance_after: f64 # Balance after this transaction currency: str # Currency code (USD) description: str @index # Human-readable description reference_type?: str # What triggered this: "payment", "kyc", "subscription", "company", "admin", "ai_call" reference_id?: str @index # ID of the triggering entity (payment_id, resident_id, etc.) external_call_id?: str @index # S59/D-017 — stable external idempotency key (e.g. aibroker call_id). Single-field @index + leader-elected poll guards double-debit; OSIS has no @unique primitive. status: CreditTransactionStatus # pending, completed, failed created_at: otime # Creation timestamp } # Digital Resident entity [rootobject] DigitalResident = { sid: str # SmartID for storage (internal ID) resident_id: str @index # Formatted ID like ZDFZ-2026-RA000001 # Owner user reference. Optional as of π-2a (D-019, s61): partner-onboarded DRs have # user_id=None — they are typed actors under Partner, not freezone users (principle 4). # All pre-s61 rows have Some(_) — migration is additive, no rewrite of existing data. user_id?: str @index # Partner that onboarded this DR (π-2a, D-019, s61). None for freezone-native DRs. # Ownership comparisons MUST use (user_id, partner_id) tuple — never bare user_id — # because None==None would otherwise create cross-partner authorisation bypass. partner_id?: str @index email: str @index # Email address first_name: str @index # First name last_name: str @index # Last name kyc_provider_id: str # ID from KYC provider (iDenfy) phone: str # Phone number status: ResidentStatus # Current status public_key?: str # Ed25519 public key encrypted_private_key?: str # AES-256-GCM encrypted private key keypair_version: i32 # Keypair version number subscription_plan?: str # Subscription plan: 6_months, 1_year, 2_years, 3_years auto_renewal: bool # Whether auto-renewal is enabled subscription_start_date?: otime # When the current subscription period started subscription_end_date?: otime # When the current subscription period ends # Payment receiving setup stripe_connected_account_id?: str # Stripe Connect account ID for receiving payments clickpesa_merchant_id?: str # ClickPesa merchant ID for receiving payments payment_receiving_enabled: bool # True once payment receiving is verified # Forward-compat fields (session 67, D-025 per D-024 invariant 6). No M1 logic reads # these; they ship Option-None on every row so M2 (#345) and M3 (#346) don't backfill. # Capture mechanism for the first three is pending Alex clarification ([NEEDS-ALEX-Q23/24/25] # in D-025) — schema slot ships now to avoid a migration when the capture path resolves. is_tanzanian_citizen?: bool # Tax Code Art. 7(c) + Art. 17 double-tax-credit branch (M3 reads) physical_residence_zanzibar?: bool # Tax Code Art. 6 (183-day test) + Art. 7(a)(i)/(ii) rate selection (M3 reads) risk_tier?: RiskTier # iDenfy AML score bucket per Compliance WP Annexe B (M2 ComplianceReview reads) kyc_refreshed_at?: otime # Set on initial KYC + each refresh (M2 cron reads) kyc_next_refresh_at?: otime # kyc_refreshed_at + tier_interval per WP §3.C (M2 cron scans) # External Companies & Proxy track (home#475). All Option/bare-bool so the ~20 existing # prod rows still deserialize. No handler reads these yet — proxy/admin-superpower # handlers land later; the anonymity resolver/UI is gated on a pending legal review. resident_type?: ResidentType # home#475 — personal | external_company | freezone_company | partner_created source?: ResidentSource # home#475 — provenance class, decoupled from partner_id # Opaque public ID (home#472). Minted later by the FF1+HMAC path; the slot ships now so # home#472 co-lands without a second migration. None until backfill mints it. external_id?: str @index # home#472 — full opaque public ID, e.g. "ZDFZ-FDS4321A" external_id_counter?: i64 @index # home#472 — decrypted counter (ops reverse-lookup) # Subject-side anonymity flag (home#473). Bare bool: the OSIS core generator emits # #[serde(default)] on bools, so existing rows deserialize to false. The resolver/Profile # UI is gated on a pending legal review; this is schema-only co-landing. anonymity_enabled: bool # home#473 — masks T1/T2 viewers when true (T3 token unaffected) anonymity_changed_at?: otime # home#473 — set on each flag flip created_at: otime # Creation timestamp updated_at: otime # Last update timestamp } # Dividend distribution [rootobject] DividendDistribution = { sid: str # SmartID for storage (dividend_id) company_id: str @index # Reference to FreeZoneCompany total_amount: f64 # Total amount to distribute currency: str # Currency code source_account_id: str # Treasury account to pay from distribution_date: otime # When dividends will be paid status: DividendStatus # Proposed, Approved, Processing, Completed, Cancelled requires_vote: bool # Whether distribution requires vote proposal_id?: str # Reference to Proposal if vote required initiated_by: str # Resident ID who initiated approved_at?: otime # When distribution was approved completed_at?: otime # When distribution completed created_at: otime # Creation timestamp updated_at: otime # Last update timestamp } # Individual dividend payment [rootobject] DividendPayment = { sid: str # SmartID for storage (payment_id) distribution_id: str @index # Reference to DividendDistribution company_id: str @index # Reference to FreeZoneCompany recipient_id: str @index # Recipient resident ID ownership_percentage: f64 # Ownership % at time of distribution amount: f64 # Amount to receive currency: str # Currency code destination_account_id?: str # Treasury account to receive funds transaction_id?: str # Reference to TreasuryTransaction paid_at?: otime # When payment was made created_at: otime # Creation timestamp } # DocumentAudience — the visibility primitive from FREEZONE.md principle 2. # Records *what* a document is (classification + relevant-period window) so that # `can_view(subject_id, document_ref)` can compute "may this subject read this # document right now?" from (CompanyRole rows × classification) on demand. # # Principals are deliberately NOT stored — computing them from CompanyRole on every # read is what keeps the answer coherent as roles change (principle 3: past parties # retain view for their period of interest; future parties do not gain retroactive view). # # Append-only like CompanyRole: if the document's classification or relevant-period # changes, write a new row with `supersedes` pointing at the prior sid. Never edit in # place. # # Relevant-period semantics (the principle-3 invariant): # - document_relevant_from: usually = document.created_at / signed_at / issued_at # - document_relevant_to: None = document is still live; Some(t) = closed at t # (supersession, termination, cancellation). `can_view` # checks whether a CompanyRole's (valid_from, valid_to) # window OVERLAPS this window — so a past party who held # the role during any part of the document's live period # retains view, and a future party who joined after the # document closed does NOT gain view. # [rootobject] DocumentAudience = { sid: str # SmartID for storage document_ref: str @index # Formatted document ID (type-prefix discriminated) document_type: DocumentKind @index # Which OSIS entity the ref points at company_id: str @index # Formatted company ID the document belongs to classification: [str] # Audience tiers this doc is visible to # (symmetric to CompanyRole.classification_scope: # view iff non-empty intersection) audience_computation: str # Rule identifier for audit trail # (e.g. "shareholders_at_signing", "coop_members_at_issue") document_relevant_from: otime # Inclusive start of document's live window document_relevant_to?: otime # Exclusive end; None = document is still live as_of_epoch: otime # When this audience row was computed supersedes?: str # sid of prior DocumentAudience row this replaces created_at: otime created_by: str # Formatted ID of the actor who wrote this row } # Email verification token [rootobject] EmailVerificationToken = { sid: str # SmartID for storage user_id: str @index # Reference to User token_hash: str @index # sha256(raw verification token) expires_at: otime # Token expiration used: bool # Whether token was used created_at: otime } # ─────────────────────────────────────────────────────────────────────────────── # ENTITY CREATION — Multi-sig draft + invite/countersign (D2) # ─────────────────────────────────────────────────────────────────────────────── # Entity draft — a company or cooperative being formed, before all parties sign [rootobject] EntityDraft = { sid: str entity_type: str @index # "company" or "cooperative" entity_name: str @index # Company/coop name company_type: CompanyType # startup_fzc, growth_fzc, global_fzc, coop_standard_fzc, etc. creator_user_id: str @index # Founder's user SID creator_dr_id: str # Founder's DR SID template_data: str # JSON: filled form fields from dynamic schema rendered_documents: str # JSON: array of {contract_id, name, content} shareholders: [ShareholderData] # Share allocation (companies) or founding members (coops) status: EntityDraftStatus # draft → pending_signatures → all_signed → paid → active document_version: i32 # Increments on edit, invalidates signatures signature_deadline: otime # Set by founder (default: 30 days) total_required_signatures: i32 # How many people must sign completed_signatures: i32 # How many have signed so far form_schema_id?: str # Reference to form schema used (for audit) created_at: otime finalized_at?: otime # When draft was finalized (invites sent) completed_at?: otime # When all signatures collected paid_at?: otime # When payment confirmed } # Invitation to join an entity as shareholder or founding member [rootobject] EntityInvitation = { sid: str draft_id: str @index # Reference to EntityDraft entity_name: str # Company/coop name (denormalized for display) inviter_user_id: str @index # Internal SID — DO NOT render to UI inviter_dr_id?: str # Inviter's formatted DR id (ZDFZ-2026-RA…) — denormalized for display inviter_name?: str # Inviter's full name — denormalized for display invitee_user_id: str @index # Internal SID — DO NOT render to UI invitee_email: str @index # For lookup + email notification (DR canonical account email) invitee_dr_id: str # Invited person's formatted DR id (FROZEN at finalize) cap_table_email: str # home#629: founder-typed cap-table row email; stable per-draft correlator so pay_and_activate binds invitee_dr_id onto the exact draft.shareholders[] row (compiled type carries #[serde(default)] — PRESERVE on hand-sync) role: str # "shareholder", "founding_member", "director" share_units: f64 # Absolute ownership/voting units (0 for coops) status: ShareholderInviteStatus # pending | accepted | rejected | expired document_version: i32 # Which draft version they signed (must match) signature_id?: str # ContractSignature SID after signing invited_at: otime responded_at?: otime expires_at: otime # = draft.signature_deadline } # Crypto exchange transaction [rootobject] ExchangeTransaction = { sid: str # SmartID for storage (exchange_id) owner_type: EntityType # Resident or company owner_id: str @index # Owner ID from_account_id: str @index # Source account to_account_id: str @index # Destination account from_amount: f64 # Amount being exchanged from_currency: str # Source currency to_amount: f64 # Amount received to_currency: str # Destination currency exchange_rate: f64 # Exchange rate used fee_percentage: f64 # Fee percentage fee_amount: f64 # Fee amount in from_currency status: ExchangeStatus # Pending, Processing, Completed, Failed, Expired rate_expires_at: otime # When quoted rate expires initiated_by: str @index # Resident ID who initiated completed_at?: otime # When exchange completed created_at: otime # Creation timestamp updated_at: otime # Last update timestamp } # External Company Registration (ECR) — the KYB record for a company outside the freezone # (home#475). The company is NOT a separate actor: after KYB it is represented BY a Digital # Resident (linked_dr_id). Admin-driven in this backend (admin superpowers); the # partners_gateway is orthogonal. Manual verification first (kyb_status). [rootobject] ExternalCompanyRegistration = { sid: str # SmartID for storage (internal ID) legal_name: str @index # Registered legal name of the external company jurisdiction: str # Country/jurisdiction of incorporation registration_number: str @index # Official registration number in its jurisdiction kyb_status: KybStatus # pending | in_progress | approved | rejected documents: [str] # Links/refs to collected KYB documents approved_by?: str # Admin who approved KYB (set on approval) linked_dr_id?: str @index # The external-company DR this ECR represents (set on link) # home#477 — provider-backed KYB (iDenfy business verification). Additive, # Option/serde-default so existing rows deserialise unchanged. The manual # kyb_status path is untouched; these record the async provider verdict that # approve_kyb consults (advisory by default; KYB_HARD_GATE makes it binding). kyb_scan_ref?: str @index # Provider session ref — correlates the async webhook back to this ECR kyb_provider?: str # Provider name that ran the check (e.g. "idenfy") kyb_provider_status?: str # Raw provider verdict (e.g. APPROVED | DENIED | SUSPECTED | PENDING) kyb_checked_at?: otime # When the provider verdict was last recorded (webhook receipt) created_at: otime # Creation timestamp updated_at: otime # Last update timestamp } # Global opaque-ID counter (home#472 Phase 2 / D-065). A SINGLE persisted monotonic # counter shared across ALL entity types (DR, company, coop) — burying every entity's # creation in one stream so a scraper collecting opaque IDs cannot estimate per-type # volume. Allocated once per minted entity: read the singleton, take next_counter, # increment, persist. This is a single global value, NOT a per-record indexed field — # an indexed plaintext counter would re-leak ordering/volume (see decisions/065). Exactly # one row exists at runtime (retrieved via list()); new entity, zero existing rows. Not # exposed over JSON-RPC (internal allocator only). [rootobject] ExternalIdState = { sid: str # SmartID for storage (the singleton row) next_counter: i64 # Next global counter to mint (monotonic; first mint = 1) } # ─────────────────────────────────────────────────────────────────────────────── # FINANCIAL REPORTS # ─────────────────────────────────────────────────────────────────────────────── # Generated financial report [rootobject] FinancialReport = { sid: str # SmartID for storage (report_id) company_id: str @index # Reference to FreeZoneCompany report_type: FinancialReportType # Balance sheet, Income statement, Cash flow, Trial balance title: str @index # Report title period_start?: otime # Start of reporting period period_end?: otime # End of reporting period as_of_date?: otime # Point-in-time date (for balance sheet) content: str # Report content (JSON or markdown) generated_by: str # Resident ID who generated created_at: otime # Creation timestamp } # Free Zone Company entity [rootobject] FreeZoneCompany = { sid: str # SmartID for storage (internal ID) company_id: str @index # Formatted ID: ZDFZ-2026-YA-PA######, Y for company, P for coop (rollover at 999,999) user_id: str @index # Owner user reference (always stamped; partner-onboarded companies use PartnerConfig.wallet_user_id) # Partner that onboarded this company (π-2a, D-019, s61). None for freezone-native companies. # Used with user_id as an ownership tuple — see DigitalResident.partner_id. partner_id?: str @index # Forward-compat field (session 67, D-025 per D-024 invariant 6). Populated when the # introducing partner is a Venture Creator per Tax Code Art. 7(a)(iv) (0%-for-10-years # window). No M1 logic reads this; ships Option-None on every row. Depends on a M2 # extension to PartnerConfig (venture_creator: bool flag — not yet present; see #332). venture_creator_id?: str # PartnerConfig key that introduced the company iff that partner is a Venture Creator # Canonical owner DigitalResident (home#475 step F / D-062). None on legacy rows ⇒ owner = DR of user_id # (today's behavior, unchanged). Set to the represented company DR's resident_id for companies an act-as # proxy creates on behalf of an external-company DR. user_id stays the real billing/operator user_id (the # acting proxy / partner wallet); ownership is keyed on the DR via this axis. Additive/Option ⇒ migration-safe. owner_resident_id?: str @index name: str @index # Company name company_type: CompanyType # Type of company voting_model: VotingModel # Standard (by shares) or Cooperative (1 vote each) description: str @index # Company description registration_number: str # Official registration number tax_id: str # Tax ID address: Address # Company address (embedded) shareholders: [ShareholderData] # Rich shareholder data crypto_wallets: [CryptoWallet] # Company crypto wallets status: CompanyStatus # Company status email?: str @index # Contact email (indexed for read-time Contact internal_kind resolution — home#412) phone?: str # Contact phone website?: str # Company website URL # Public URL to the company logo image; when set, the invoice PDF renders it in the # page header alongside the company name (home#367). External URL keeps storage simple # (binary upload = file-storage layer, M2+). Additive/Option ⇒ migration-safe. logo_url?: str # Company logo image URL (invoice PDF header) industry?: str # Business sector public_key?: str # Ed25519 public key encrypted_private_key?: str # AES-256-GCM encrypted private key keypair_version: i32 # Keypair version number # Subscription fields subscription_plan?: CompanySubscriptionPlan # Monthly, 1yr, or 2yr subscription_start_date?: otime # When subscription started subscription_end_date?: otime # When subscription ends auto_renewal: bool # Whether to auto-renew # Pricing snapshot at registration setup_fee_paid: f64 # Setup fee paid (after discounts) monthly_fee_rate: f64 # Base monthly fee rate shareholder_fee_rate: f64 # Per-shareholder fee rate (0 if N/A) prepaid_discount_applied: f64 # Discount percentage applied (0.0, 0.2, or 0.4) early_bird_applied: bool # Whether early bird discount was applied draft_id?: str # Reference to EntityDraft (set during multi-sig creation) # Payment receiving setup stripe_connected_account_id?: str # Stripe Connect account ID for receiving payments clickpesa_merchant_id?: str # ClickPesa merchant ID for receiving payments payment_receiving_enabled: bool # True once payment receiving is verified # Governance due-process executor config (session 55). Coop variants only; None falls # back to branding.toml [governance.due_process.default_executors] at handler time. due_process_executors?: CoopDueProcessExecutors # External Companies & Proxy track (home#475). Option/bare-bool — additive, deserialize-safe # for existing rows. Cooperative is a FreeZoneCompany subtype (company_type in # {coop_standard_fzc, coop_platinum_fzc}), so the coop variant inherits these here — there # is no separate Cooperative rootobject. external_id?: str @index # home#472 — opaque public ID (minted later) external_id_counter?: i64 @index # home#472 — decrypted counter (ops reverse-lookup) anonymity_enabled: bool # home#473 — masks T1/T2 on the company verify card (schema-only in M1; no Company toggle UI) anonymity_changed_at?: otime # home#473 — set on each flag flip license?: CompanyLicense # OWZ-issued license (admin grant, discretionary). None = unlicensed (verify/QR unchanged). Drives the verify-page license line + the downloadable License PDF. # Registrar "Void registration" supersede fields (D-071, home#498). All None on live # companies; set atomically when an admin voids an erroneous registration (status -> voided). # Immutability: the record is retained + marked, never edited away. void_reason?: str # Why the registration was voided (mandatory at void time) voided_at?: otime # When the registration was voided voided_by?: str # Admin (whitelisted email) who voided it created_at: otime # Creation timestamp updated_at: otime # Last update timestamp } # sid + created_at + updated_at are the auto-added rootobject header (not declared here) # Free Zone Expense entity [rootobject] FreeZoneExpense = { sid: str # SmartID for storage (fz_expense_id) fzc_id: str @index # Reference to FreeZoneCompany receipt_number: str @index # Receipt number vendor_name: str @index # Vendor name description: str @index # Expense description amount: f64 # Expense amount currency: str # Currency code category: str # Expense category receipt_url?: str # URL to receipt image expense_date: otime # Expense date status: ExpenseStatus # Expense status created_at: otime # Creation timestamp updated_at: otime # Last update timestamp } # Free Zone Invoice entity [rootobject] FreeZoneInvoice = { sid: str # SmartID for storage (fz_invoice_id) fzc_id: str @index # Reference to FreeZoneCompany reseller_invoice_id: str # Reference to reseller system invoice_number: str @index # Invoice number for display amount: f64 # Total invoice amount currency: str # Currency code (USD, EUR, etc.) tax_amount: f64 # Tax amount description: str @index # Invoice description line_items: [LineItem] # List of invoice items issue_date: otime # Issue date due_date: otime # Due date status: InvoiceStatus # Invoice status sender_user_id?: str @index # User who created/sent the invoice (P2P) sender_entity_type?: str # "company" or "cooperative" (null = DR) sender_entity_id?: str @index # FZC/coop SID if sending on behalf of entity (D-043: indexed for sender-entity invoice lookup) recipient_user_id?: str @index # User who should pay the invoice (P2P) recipient_entity_type?: str # "resident" or "company" or "cooperative" recipient_entity_id?: str @index # DR ID or Company/Coop ID (D-043: indexed for recipient-entity invoice lookup) recipient_email?: str @index # External recipient email (D-043: indexed for find_by_email path) payment_instructions?: InvoicePaymentInstructions # Out-of-band settlement details shown on the PDF # Sender-authored personal note delivered in the invoice email (home#641). # STORED (unlike issuer_name/recipient_name below): it must survive the write so a # send retried from the invoice list carries the same note the sender composed. A # per-request send param would silently deliver a noteless invoice on every retry # (send_draft leaves a failed external send as Draft, by design — home#635). # Body-only: html_escape_multiline'd at the render_invoice_sent_email choke point, # never in the subject and never in a header. Capped at MAX_SENDER_MESSAGE_LEN. sender_message?: str # Sender's personal note shown in the invoice email payment_method?: str # Completion method (e.g. "manual"; set when marked paid) payment_reference?: str # Completion reference (set when marked paid) paid_at?: otime # When payment was completed created_at: otime # Creation timestamp updated_at: otime # Last update timestamp # Derived, read-only issuer attribution (home#595). NEVER stored — populated at list # read time from sender_entity_id -> FreeZoneCompany (name + registration_number). # None for DR-issued invoices or unresolved companies. Mirrors Contact.internal_kind # (round-trips as None through freezoneinvoice_set; skip_serializing keeps it out of storage). issuer_name?: str # Resolved issuing-company display name (derived; never stored) issuer_registration_number?: str # Resolved issuing-company registration number (derived; never stored) # Derived, read-only recipient attribution (home#636). NEVER stored — populated on # list AND detail read paths. Unlike the issuer fields this is ALWAYS Some(..) on a # read path: the To cell cannot omit the recipient, so a None here makes the frontend # fall back to the raw recipient_user_id and the SID leak survives. Resolution order: # company/coop entity -> DigitalResident real name -> User name -> email -> "—". recipient_name?: str # Resolved recipient display name (derived; never stored) } # Frontend error report — captured from user-facing app [rootobject] FrontendError = { sid: str # SmartID for storage user_id: str @index # User who hit the error (empty if pre-login) error_code: str @index # Error category (network, auth, validation, server, etc.) page: str @index # Page/route where error occurred message: str # Error message shown to user user_agent: str # Browser user-agent string ip_address: str # Client IP (for rate limiting dedup) created_at: otime # When the error occurred } # ─────────────────────────────────────────────────────────────────────────────── # TREASURY OPERATIONS # ─────────────────────────────────────────────────────────────────────────────── # Fund transfer between accounts [rootobject] FundTransfer = { sid: str # SmartID for storage (transfer_id) from_account_id: str @index # Source treasury account to_account_id?: str @index # Destination treasury account (internal) to_external_address?: str # External bank/wallet address to_external_name?: str # External recipient name amount: f64 # Transfer amount currency: str # Currency code reference?: str # Payment reference status: FundTransferStatus # Pending, Processing, Completed, Failed, Cancelled requires_approval: bool # Whether transfer needs approval approval_threshold: f64 # Amount above which approval is needed approved_by?: str # Resident ID who approved initiated_by: str @index # Resident ID who initiated transaction_id?: str # External transaction ID fee_amount?: f64 # Transfer fee completed_at?: otime # When transfer completed created_at: otime # Creation timestamp updated_at: otime # Last update timestamp } # GovernanceAction — coop member discipline due-process step (session 55, #288, Alex # Q3 resolved 2026-04-22 at #308#22569). The coop-member-discipline equivalent of the # shareholder-voting flow (Proposal/Vote/Resolution). Orthogonal primitives — do NOT # merge: Proposal/Vote/Resolution = voting mechanics; GovernanceAction = 5-step due # process for member discipline. # # Each of the 5 Bylaws steps is a new row chained via parent_action_sid: # notice → response → review → decision → appeal # An appeal RULING is itself a `decision` row with parent_action_sid = appeal_sid; # the parent's kind discriminates which executor set applies (Step-4 for review- # parented decisions; Step-5 for appeal-parented decisions). # # Append-only (principle 3): never mutate a row. decided_at / decided_by / # outcome are populated at write time for kind=decision rows and never change # afterwards. Enforcement of `outcome=remove` is a SEPARATE handler # (`enforce_decision`) that calls `companyroleservice.remove_member` and writes a # single UserAuditLog row — it does NOT mutate the decision row and does NOT write # a new GovernanceAction row. This was a B.5 catch (see decisions/013): # - Collapsing closure into `issue_decision` would cross the OSIS multi-entity # transaction boundary — partial failure leaves a decision row with no paired # closure, and append-only forbids re-trying the decision. # - Opportunistic-on-next-call closure would leak cross-subject side effects # (filing a notice on member B could close member X's role as a side effect of # a handler that never mentioned X). # Therefore enforcement is an explicit, idempotent, caller-authorized handler; # D-011's monotonic "already-closed" guard on CompanyRole makes retry a no-op. # [rootobject] GovernanceAction = { sid: str # SmartID for storage company_id: str @index # Formatted coop ID (ZDFZ-YYYY-YP... or ZDFZ-YYYY-P...) subject_resident_id: str @index # Formatted DR ID of the member under discipline kind: GovernanceActionKind @index # Which step in the 5-step chain parent_action_sid?: str @index # Prior step in chain; None iff kind=notice content: str # Free-form JSON-serialized step content due_by: otime # When this step's SLA expires (computed at write # from prior step's due_by + branding.toml SLA) decided_at?: otime # When the ruling was issued (kind=decision only) decided_by_role?: ExecutorRole # Executor set the ruler belonged to (decision only) decided_by_subject_id?: str # Formatted ID of the individual who ruled (decision only) outcome?: DecisionOutcome # The ruling (kind=decision only; None otherwise) created_at: otime created_by: str # Formatted ID of the actor who wrote this row } # Per-user reusable invoice payment-instruction defaults (home#596 Slice B). # A sender (issuing user) saves their "how to pay me" details once; the invoice # wizard prefills them onto new invoices instead of re-entering every time. # Storage-only (no service block -> no auto JSON-RPC); the 3 hand-added gated # methods (get_payment_defaults / set_payment_default / delete_payment_default) # live on InvoiceServiceHandler and are dispatched by hand (mirroring send_invoice). # owner_user_id is NOT @index: the store is tiny (~one row per issuing user) so the # handlers list-and-filter (like ExternalIdState), avoiding a Tantivy indexer wire-up. # Secret handling mirrors the invoice payment_instructions choke point: the plaintext # security_answer is encrypted on save (normalize_invoice_payment_instructions) and # every read redacts security_answer{,_encrypted} (redact_invoice_payment_secrets), so # the stored answer is never-read-back. Prefill copies the stored default VERBATIM into # normalize's `existing` arg (NEVER re-normalizes: normalize rebuilds output from the # plaintext security_answer only and ignores an already-encrypted one). [rootobject] InvoicePaymentDefault = { sid: str # SmartID for storage (server-assigned) owner_user_id: str # Owning issuer user_id (caller-gated; list-filter key) sender_entity_type?: str # "company" | "cooperative" | null=DR (mirrors invoice sender) sender_entity_id?: str # FZC/coop SID if the default is for an entity's invoices currency?: str # Currency this default applies to (matched at prefill; null = any) label?: str # Optional user label for this default payment_instructions: InvoicePaymentInstructions # The saved "how to pay" payload (secret encrypted at rest) } # Session 63 (D-021): partner-track π-4 delegated-KYC attestation row (IMMUTABLE side). # Carries the audit-significant metadata: partner identity, partner-supplied signature, # SHA-256 hash over the encrypted payload, encryption scheme label, and key_id used. # Principle 3 immutable — never edited in place, never scrubbed. Paired 1:1 with a # KycArtifactPayload row (separately lifecycled; scrubbable for GDPR Art. 17). The # attestation outlives the payload: after erasure, the attestation + payload_hash still # prove "partner P attested to artifact H at time T" even if the plaintext is gone. # RegistrationRequest.kyc_attestation_sid points HERE, never at the payload. # Signature is stored verbatim in M1 — never verified (no partner-pubkey infra yet). # See decisions/021-partner-track-kyc-artifact.md. [rootobject] KycArtifactAttestation = { sid: str # SmartID for storage request_id: str @index # Reference to RegistrationRequest.sid partner_id: str @index # Partner name (e.g. "tftc") — denormalized from RegistrationRequest at submit partner_signature: str # Partner's signed attestation string — STORED VERBATIM, never verified in M1 (same pattern as s62 synthetic admin_signature). M2 adds partner-pubkey + Ed25519 verify. payload_hash: str @index # SHA-256 hex over the encrypted_payload bytes at submit time. Integrity check survives payload erasure — auditors can verify "hash H matches what partner attested to" even after the blob is scrubbed. encryption_scheme: str # Label identifying the encryption format (M1: "aes-256-gcm-v1"). Future-proofs for when X25519 asymmetric lands in M2. key_id: str @index # Which partner artifact_key was used (from [partner_config.].artifact_keys map). Survives key rotation — pre-rotation artifacts stay decryptable as long as the historical key is retained. created_at: otime # Creation timestamp } # Session 63 (D-021): partner-track π-4 delegated-KYC payload row (ERASABLE side). # Carries the PII: the encrypted payload itself. Separately lifecycled from the # attestation so a GDPR Art. 17 right-to-erasure can scrub `encrypted_payload` + stamp # `erased_at` + `erasure_reason` without editing the principle-3 immutable attestation. # When erased: encrypted_payload becomes empty string + erased_at is Some; the attestation # survives unmodified. Admin read-path (π-5, #331) branches on `erased_at.is_some()`: # on erased rows, surface a tombstone + erasure_reason instead of attempting decrypt. # Erasure RPC itself is M2 (no admin UI yet); schema shape supports it TODAY so no # principle-3 migration is needed when the RPC lands. # See decisions/021-partner-track-kyc-artifact.md. [rootobject] KycArtifactPayload = { sid: str # SmartID for storage attestation_sid: str @index # Reference to KycArtifactAttestation.sid (1:1) encrypted_payload: str # AES-256-GCM ciphertext (hex(nonce):hex(ct) per existing main.rs::decrypt_aes256gcm format). Partner encrypts CLIENT-SIDE with PartnerConfig.artifact_keys[key_id]; freezone stores verbatim. Empty-string when erased. key_id: str @index # Redundant with attestation.key_id but indexed here too so payload-only lookups work during rotation sweeps. erased_at?: otime # When the payload was scrubbed under GDPR / retention policy. None for live artifacts. erasure_reason?: str # Human-readable reason ("gdpr_erasure" | "retention_expired" | "refused_application" | free-form). None for live artifacts. created_at: otime # Creation timestamp } # KYC Personal Data — verified iDenfy PII fields, encrypted at rest [rootobject] # https://forge.ourworld.tf/znzfreezone_code/home/issues/387 # Captured per iDenfy webhook when [features.kyc_persistence] enabled. Master + per-field # toggles in branding.toml. Optional fields stored as None when toggled off; existing rows # stay sparse on subsequent toggle-on (no backfill — refresh-cadence scheduler is umbrella #386). KycPersonalData = { sid: str # SmartID for storage resident_id: str @index # Reference to DigitalResident user_id: str @index # Reference to User kyc_provider_id: str # iDenfy scan_ref (denormalized for audit) dob?: str # Date of birth (encrypted at rest when encrypt_at_rest=true) nationality?: str # Nationality (encrypted at rest) document_number?: str # Document number (encrypted at rest) document_type?: str # Document type (encrypted at rest) document_expiry?: str # Document expiry (encrypted at rest) created_at: otime # Creation timestamp updated_at: otime # Last update timestamp } # KYC Session entity [rootobject] KycSession = { sid: str # SmartID for storage (session_id) user_id: str @index # Reference to User resident_id: str @index # Reference to DigitalResident public_key: str # Public key used for verification kyc_url: str # URL to KYC service status: KycStatus # Verification status idenfy_scan_ref?: str @index # iDenfy scan reference (D-043: indexed for kycsession_find scan_ref lookup) identity_verified: bool # Identity verification result document_verified: bool # Document verification result liveness_verified: bool # Liveness check result verification_level?: str # Verification level (basic, full, etc.) callback_url?: str # Callback URL for completion expires_at: otime # Session expiration verified_at?: otime # When verification completed created_at: otime # Creation timestamp updated_at: otime # Last update timestamp } # MCP approval queue entry [rootobject]. # Session 65 adds result_json / decision_reason / execution_error to support the # decision flow: result_json carries the tool's output after ApprovedExecuted so # the MCP caller can poll the row for the answer; decision_reason is the # user-supplied reason on reject; execution_error captures why a dispatch failed # after approve (e.g. "ApiKey revoked between approve and execute" — the row # stays Approved, not ApprovedExecuted, so a later retry is possible once the # underlying condition is fixed). See decisions/023-mcp-tier6-approval.md. McpApproval = { sid: str # SmartID for storage user_id: str @index # Who needs to approve (owner of the API key) api_key_id: str @index # Which key triggered it tool_name: str @index # Requested MCP tool name (e.g. "freezone_shareholderservice_execute_dividend") # Resolved OSIS RPC method (e.g. "shareholderservice.execute_dividend"). Stamped at # approval-creation time from the matched McpTool's `rpc_method`. Used by # execute_approved to dispatch — required because tool_name (MCP format) is NOT # accepted by the OSIS dispatcher, which expects ".". Optional # for forward-compat: legacy rows with rpc_method=None fall back to using # tool_name (current pre-fix behaviour, intentionally still broken — production # is locked per §0 and no legacy rows exist). rpc_method?: str arguments: str # Full arguments JSON (not redacted; replayed at execute time) amount: f64 # Financial amount reason: str # Why approval needed (from needs_approval() at create time) status: McpApprovalStatus @index # pending / approved / approved_executed / rejected / expired requested_at: otime @index # When requested decided_at?: otime # When approved/rejected/expired (stamped on terminal transition) expires_at: otime # Auto-expire (default 1 hour) created_at: otime # Creation timestamp result_json?: str # Tool output JSON after successful execute (present iff status=ApprovedExecuted) decision_reason?: str # User-supplied reason on reject execution_error?: str # Dispatch error after approve (e.g. "ApiKey revoked"); row stays Approved } # MCP call audit log [rootobject] McpCallLog = { sid: str # SmartID for storage api_key_id: str @index # Which API key made the call user_id: str @index # Which user owns the key tool_name: str @index # Tool that was called arguments_summary: str # Sanitized args (no secrets) amount: f64 # Financial amount (0 if non-financial) result_status: str @index # success / error / rejected error_message: str # Error detail if rejected/failed created_at: otime @index # Timestamp } # Self-service 2FA recovery session — binds a user to a fresh iDenfy scan opened # to re-prove identity after losing their device + all backup codes. [rootobject] # https://forge.ourworld.tf/znzfreezone_code/home/issues/550 # Stored type, in-process accessors ONLY — deliberately NOT in ROOTOBJECT_TYPES # (no wire CRUD; lookups go through the linear-scan find_fallback, mirroring # Proxy / ExternalCompanyRegistration). recovery_id is the opaque SID. MfaRecoverySession = { sid: str # SmartID for storage (the opaque recovery_id) user_id: str @index # Reference to User (server-derived from the MFA challenge) resident_id: str # Reference to DigitalResident (KYC subject) idenfy_scan_ref: str @index # iDenfy scan reference (webhook lookup key) status: str # pending | approved | rejected | expired created_at: otime # Creation timestamp expires_at: otime # Session expiration (30 min) updated_at: otime # Last update timestamp } # (usually == subject_id; may differ when admin/system acts on behalf) # Password reset token [rootobject] PasswordResetToken = { sid: str # SmartID for storage token_hash: str @index # sha256(raw reset token) email: str @index # User email expires_at: i64 # Unix epoch expiry used: bool # One-time use flag created_at: otime } # Payment entity for crypto/ClickPesa [rootobject] Payment = { sid: str # SmartID for storage (payment_id) user_id: str @index # Reference to User entity_id: str @index # Resident or Company ID entity_type: EntityType # Resident or Company (enum) payment_type: PaymentType # Bitcoin, Ethereum, ClickPesa amount: f64 # Amount in currency currency: str # Currency code (USD, etc.) crypto_amount?: str # Formatted crypto amount exchange_rate?: str # Exchange rate at time of payment wallet_address?: str # Destination wallet address sender_address?: str # Sender's address transaction_hash?: str # Blockchain transaction hash confirmation_count?: i32 # Block confirmations external_id: str # External payment provider ID clickpesa_order_id?: str @index # ClickPesa order ID (indexed for webhook lookup) payment_purpose: str # Purpose (resident_subscription, company_registration) invoice_id?: str # Invoice SID this row settles when payment_purpose == "invoice_payment" (dedicated row->invoice link; entity_id/entity_type stay a genuine typed-actor owner) — home#546 subscription_plan?: str # Subscription plan if applicable auto_renewal: bool # Whether auto-renewal was requested status: PaymentStatus # Payment status expires_at: otime # Payment expiration confirmed_at?: otime # When payment was confirmed created_at: otime # Creation timestamp updated_at: otime # Last update timestamp } # ─────────────────────────────────────────────────────────────────────────────── # GOVERNANCE & VOTING # ─────────────────────────────────────────────────────────────────────────────── # Governance proposal [rootobject] Proposal = { sid: str # SmartID for storage (proposal_id) company_id: str @index # Reference to FreeZoneCompany title: str @index # Proposal title description: str # Full proposal description (markdown) category: ProposalCategory # Financial, Strategic, Operational, Governance, Personnel kind: ResolutionKind @index # Ordinary, Special, or Board — drives threshold comparator (session 56 / D-014) status: ProposalStatus # Draft, Active, Passed, Rejected, Expired, Executed proposer_id: str @index # Resident ID who created proposal voting_starts_at: otime # When voting begins voting_ends_at: otime # When voting ends quorum_percentage: i32 # Required participation percentage (0-100) — snapshotted from branding.toml at creation per kind passing_threshold: i32 # Required "for" percentage to pass (0-100) — snapshotted from branding.toml at creation per kind total_issued_power_snapshot: f64 # Total issued voting power at create_proposal time. Standard = sum of ShareholderData.units; Cooperative = count of open founding_member + platinum_member CompanyRoles (1 unit each). Denominator for quorum check at tally time. Frozen by proposal.set immutability guard. allow_abstention: bool # Whether abstention counts toward quorum (NOT toward passing threshold per Alex Q2 "aligned with M&A") votes_for: f64 # Total voting power for votes_against: f64 # Total voting power against votes_abstain: f64 # Total voting power abstained total_voted: f64 # Total voting power that voted resolution_id?: str # Reference to Resolution if executed created_at: otime # Creation timestamp updated_at: otime # Last update timestamp } # Proxy — act-as relation between two Digital Residents (home#475). A person (principal_dr_id, # the DR who logs in) is authorized to act on behalf of another DR (target_dr_id, e.g. an # external-company DR with user_id=None). Time-bounded per the Role + Time + Classification # principle: effective validity = status==active AND valid_from<=now AND # (valid_to is None OR now RegistrationRequestListResult error: AdminRegistrationError # Get full registration details with linked KYC, payment, contracts get_registration_details(request_id: str) -> RegistrationRequestFullDetailResult error: AdminRegistrationError # Automated check that prerequisites are met verify_registration_prerequisites(request_id: str) -> PrerequisiteCheckResult error: AdminRegistrationError # Approve registration and create official record approve_registration(request_id: str, admin_email: str, admin_signature: str) -> ApproveRegistrationResult error: AdminRegistrationError # Reject registration with explanation reject_registration(request_id: str, admin_email: str, reason: str) -> RejectRegistrationResult error: AdminRegistrationError # Request additional info from reseller request_additional_info(request_id: str, admin_email: str, message: str) -> RequestInfoResult error: AdminRegistrationError } service ApiKeyService { version: "1.0.0" description: "API key management for resellers" # Generate API key generate(client_id: str, client_secret: str, access_password: str) -> ApiKeyResult error: ApiKeyError } # ─────────────────────────────────────────────────────────────────────────────── # RPC SERVICES - Custom business logic (beyond CRUD) # ─────────────────────────────────────────────────────────────────────────────── service AuthService { version: "1.0.0" description: "User authentication and session management" # Register a new user register(email: str, password: str, first_name?: str, last_name?: str) -> AuthResult error: RegistrationError # Login user and return JWT token login(email: str, password: str) -> AuthResult error: AuthError # Refresh JWT token refresh_token(token: str) -> AuthResult error: AuthError # Fetch current user state (side-effect-free — no token rotation). # Frontends call this after mutations that change user state (KYC, email # verification, subscription renewal) to refresh their local cache. # Backend is SSOT; frontend caches are convenience, not authority. me(user_id: str) -> UserInfo error: AuthError # Send email verification send_verification_email(email: str) -> VerifyEmailResult error: EmailError # Verify email with token verify_email(token: str) -> VerifyEmailResult error: EmailError # Change password change_password(user_id: str, old_password: str, new_password: str) -> ChangePasswordResult error: AuthError # Begin TOTP 2FA enrollment — returns the shared secret + provisioning URI (home#506) user_totp_enroll_begin(user_id: str) -> TotpEnrollBegin error: AuthError # Confirm TOTP enrollment by verifying a code; returns one-time backup codes (shown once) user_totp_enroll_confirm(user_id: str, code: str) -> TotpEnrollConfirm error: AuthError # Verify a TOTP code for the caller (step-up / re-auth) user_totp_verify(user_id: str, code: str) -> TotpVerifyResult error: AuthError # Disable TOTP 2FA for the caller (requires a fresh TOTP or backup code) user_totp_disable(user_id: str, code: str) -> ChangePasswordResult error: AuthError # Complete a 2FA login challenge with a TOTP code or a backup code (home#506) login_mfa(challenge: str, code: str) -> AuthResult error: AuthError # Self-service 2FA recovery (home#550): begin re-proofing for a user who lost # their authenticator device AND all backup codes. Anon — gated by the # password-proven MFA challenge (user_id is read from the signed token, never # a caller param); opens a fresh, rate-limited iDenfy scan. There is NO admin # 2FA reset — a passing iDenfy re-verification is the sole authorizer. begin_2fa_recovery(mfa_challenge: str) -> Begin2faRecoveryResult error: AuthError # Poll the opaque status of a recovery session by its opaque recovery_id (home#550) poll_2fa_recovery(recovery_id: str) -> Poll2faRecoveryResult error: AuthError } service BusinessDocumentService { version: "1.0.0" description: "Business document management" # Upload document upload_document(company_id: str, document_type: BusinessDocumentType, name: str, description?: str, file_url: str, uploaded_by: str) -> BusinessDocumentResult error: BusinessDocumentError # Generate official document generate_official_document(company_id: str, document_type: BusinessDocumentType) -> BusinessDocumentResult error: BusinessDocumentError # Get document get_document(document_id: str) -> BusinessDocumentDetailResult error: BusinessDocumentError # List documents for a company list_documents(company_id: str, document_type?: BusinessDocumentType) -> BusinessDocumentListResult error: BusinessDocumentError # Delete document (non-official only) delete_document(document_id: str) -> DeleteBusinessDocumentResult error: BusinessDocumentError } service CompanyRoleService { version: "1.0.0" description: "CompanyRole lookups (role + time + classification, FREEZONE.md principle 2)" # List roles held at a company at a given instant. # as_of_epoch == 0 means "now". Filters by valid_from <= as_of AND (valid_to IS NULL OR valid_to > as_of). list_by_company(company_id: str, as_of_epoch: i64) -> CompanyRoleListResult error: CompanyRoleError # List roles a subject (resident or corporate shareholder) holds across all companies at a given instant. list_by_resident(subject_id: str, as_of_epoch: i64) -> CompanyRoleListResult error: CompanyRoleError } service CompanyService { version: "1.0.0" description: "Free zone company management" # List companies for user with pagination list(user_id: str, page: i32, page_size: i32) -> CompaniesListResult error: CompanyError # Create a new company create(user_id: str, data: CreateCompanyData) -> CompanyResult error: CompanyError # Get company by ID get(company_id: str) -> CompanyResult error: CompanyError # Update company update(company_id: str, data: UpdateCompanyData) -> CompanyResult error: CompanyError # Delete company delete(company_id: str) -> DeleteResult error: CompanyError # Get company card (public display info) get_card(company_id: str) -> CompanyCardResult error: CompanyError # Get company key status get_key_status(company_id: str) -> KeyStatusResult error: KeypairError # Get company encrypted private key blob (client decrypts locally) get_company_encrypted_key(company_id: str) -> EncryptedKeyResult error: KeypairError # Store client-generated company keypair (replaces existing) store_company_keypair(company_id: str, public_key: str, encrypted_private_key: str) -> KeypairResult error: KeypairError } service ContractService { version: "1.0.0" description: "Business contracts and templates" # List company contracts list_company_contracts(fzc_id: str) -> ContractsListResult error: ContractError # Get company contract get_company_contract(fzc_id: str, contract_id: str) -> ContractResult error: ContractError # Create company contract create_company_contract(fzc_id: str, data: CreateContractData) -> ContractResult error: ContractError # Update company contract update_company_contract(fzc_id: str, contract_id: str, data: UpdateContractData) -> ContractResult error: ContractError # Archive company contract archive_company_contract(fzc_id: str, contract_id: str) -> ContractResult error: ContractError # List contract templates list_templates() -> TemplatesListResult error: ContractError # Get contract template get_template(template_id: str) -> TemplateResult error: ContractError # Get required registration contracts get_required_contracts(entity_type: EntityType) -> RequiredContractsResult error: ContractError # home#539: fetch a SINGLE required registration contract (with body) by its # unique category_str key — e.g. ("resident", "terms_of_service") returns only # the ToS, so a screen rendering one contract no longer transfers the whole # required set's bodies. Returns NotFound if no required contract has that key. get_required_contract(entity_type: EntityType, category_str: str) -> RegistrationContractResult error: ContractError # home#539: metadata-only required-contracts list — same selection as # get_required_contracts but each item omits the ~36KB `content` body. For # callers (Documents name-map, AI assistant) that need names/ids, not bodies. get_required_contracts_meta(entity_type: EntityType, company_type?: str) -> RequiredContractsMetaResult error: ContractError # List every registration contract template known to the freezone — # formation contracts (M&A, Company Declaration, Bylaws) and lifecycle # templates (ma_amendment, share_transfer, buyback, expulsion, upgrade # resolution, etc.). Used by the Documents page to resolve any # ContractSignature's contract_id to a human-readable name + category, # including signatures from events that `get_required_contracts` filters # out because they are not required at creation time. list_all_contracts() -> RequiredContractsResult error: ContractError # Sign contract (no password — session auth is sufficient) sign_contract(user_id: str, contract_id: str, entity_type: str, entity_name: str, entity_id: str) -> SignContractResult error: SignError # Get contract signatures for user get_signatures(user_id: str) -> SignaturesListResult error: ContractError } service CreditService { version: "1.0.0" description: "Credit balance management — top-up, deduction, and transaction history" # Get user's credit balance (creates if not exists) get_balance(user_id: str) -> CreditBalanceResult error: CreditError # Top up credits from a confirmed payment top_up(user_id: str, amount: f64, payment_id: str) -> CreditTransactionResult error: CreditError # Deduct credits for a service (KYC, subscription, company registration) deduct(user_id: str, amount: f64, description: str, reference_type: str, reference_id: str) -> CreditTransactionResult error: CreditError # Add bonus credits (welcome bonus, promotions) add_bonus(user_id: str, amount: f64, description: str) -> CreditTransactionResult error: CreditError # Refund credits (e.g., failed KYC) refund(user_id: str, amount: f64, description: str, reference_type: str, reference_id: str) -> CreditTransactionResult error: CreditError # List credit transactions for a user list_transactions(user_id: str, page: i32, page_size: i32) -> CreditTransactionsListResult error: CreditError # Get credit pricing (costs for services) get_pricing() -> CreditPricingResult error: CreditError # Get onboarding status (progressive unlock tiers) get_onboarding_status(user_id: str) -> OnboardingStatus error: CreditError # Initiate a wallet top-up via a specific payment provider initiate_topup(user_id: str, amount: f64, provider: str) -> ClickPesaResult error: CreditError # Set up auto top-up via Stripe (saves card, sets threshold/amount) setup_auto_topup(user_id: str, amount: f64, threshold: f64) -> AutoTopupResult error: CreditError # Disable auto top-up disable_auto_topup(user_id: str) -> AutoTopupResult error: CreditError # Activate resident after credit deduction (keypair generated client-side) activate_resident(user_id: str, plan: str, public_key: str, encrypted_private_key: str) -> ActivateResult error: CreditError } service DocumentAudienceService { version: "1.0.0" description: "Document visibility (FREEZONE.md principle 2: who can read document D right now, computed from CompanyRole × classification)" # Answer "may subject_id read document_ref right now?". # Composes (CompanyRole rows where window overlaps document's relevant-period) # × (classification intersection). Returns false if no DocumentAudience row # exists for document_ref (legacy fallback policy deferred to first consumer; # see decisions/004-document-audience.md). can_view(subject_id: str, document_ref: str) -> DocumentCanViewResult error: DocumentAudienceError # List all DocumentAudience rows for a document_ref, newest first. # Used by audit trail and supersession-chain inspection. list_by_document(document_ref: str) -> DocumentAudienceListResult error: DocumentAudienceError } service DocumentService { version: "2.0.0" description: "Client-side document signing — signature submitted after browser-side Ed25519 sign" # Submit a client-signed document (signature created in WASM, server verifies with stored public key) submit_signed(entity_type: EntityType, entity_id: str, document_type: DocumentType, document_hash: str, signature: str) -> SignResult error: SignError # Verify document signature verify(signature: str, document_hash: str, signer_id: str, signer_type: EntityType) -> VerifyResult error: VerifyError # Get key status get_key_status(user_id: str) -> KeyStatusResult error: KeypairError } service EntityDraftService { version: "1.0.0" description: "Multi-stakeholder entity creation with invite/countersign flow" # Get dynamic form schema for a company/coop type get_form_schema(entity_type: str, company_type: CompanyType) -> FormSchemaResult error: DraftError # Creator: create a draft entity create_draft(user_id: str, entity_type: str, entity_name: str, company_type: CompanyType, template_data: str, shareholders: [ShareholderData]) -> DraftResult error: DraftError # Creator: preview rendered documents before finalizing preview_documents(draft_id: str) -> PreviewDocumentsResult error: DraftError # Creator: finalize draft and send invitations finalize_draft(draft_id: str) -> DraftResult error: DraftError # Creator: cancel draft cancel_draft(draft_id: str) -> DraftResult error: DraftError # Creator: get draft status with signature progress get_draft_status(draft_id: str) -> DraftStatusResult error: DraftError # Creator: list drafts for user list_drafts(user_id: str) -> DraftsListResult error: DraftError # Invitee: get my pending invitations get_my_invitations(user_id: str) -> InvitationsListResult error: DraftError # Invitee: get invitation documents for review get_invitation_documents(invitation_id: str) -> InvitationDocumentsResult error: DraftError # Invitee: accept invitation and sign accept_invitation(invitation_id: str, user_id: str) -> InvitationResult error: DraftError # Invitee: decline invitation decline_invitation(invitation_id: str, user_id: str, reason?: str) -> InvitationResult error: DraftError # Check if all signatures collected, trigger payment + activation check_completion(draft_id: str) -> CompletionResult error: DraftError # Creator: pay for entity after all signatures collected pay_and_activate(draft_id: str, user_id: str, subscription_plan: CompanySubscriptionPlan) -> DraftResult error: DraftError } service ExpenseService { version: "1.0.0" description: "Expense management for companies" # List expenses for company list(fzc_id: str) -> ExpensesListResult error: InvoiceError # Create expense create(fzc_id: str, data: CreateExpenseData) -> ExpenseResult error: InvoiceError # Get expense by ID get(expense_id: str) -> ExpenseResult error: InvoiceError # Update expense update(expense_id: str, data: UpdateExpenseData) -> ExpenseResult error: InvoiceError # Delete expense delete(fzc_id: str, expense_id: str) -> DeleteResult error: InvoiceError # Submit expense for approval submit(expense_id: str) -> ExpenseResult error: InvoiceError # Approve expense approve(expense_id: str, approved_by: str) -> ExpenseResult error: InvoiceError # Reject expense reject(expense_id: str, rejected_by: str, reason: str) -> ExpenseResult error: InvoiceError } service GovernanceService { version: "1.0.0" description: "Proposal creation, voting, and resolution management" # Create a new proposal. `kind` drives the threshold snapshot (Ordinary/Special/Board); # quorum + passing percentages are read from branding.toml [governance.resolution_thresholds] # at creation time and stamped onto the Proposal row immutably (session 56 / D-014). create_proposal(company_id: str, title: str, description: str, category: ProposalCategory, kind: ResolutionKind, voting_days: i32, allow_abstention: bool) -> ProposalResult error: GovernanceError # Get proposal details get_proposal(proposal_id: str) -> ProposalDetailResult error: GovernanceError # List proposals for a company list_proposals(company_id: str, status?: ProposalStatus) -> ProposalListResult error: GovernanceError # Cast a vote on a proposal cast_vote(proposal_id: str, voter_id: str, vote_value: VoteValue, comment?: str, password: str) -> VoteResult error: GovernanceError # Get voting power for a shareholder get_voting_power(company_id: str, resident_id: str) -> VotingPowerResult error: GovernanceError # Tally votes and update proposal status tally_votes(proposal_id: str) -> TallyResult error: GovernanceError # Execute a passed proposal (create resolution) execute_proposal(proposal_id: str, executor_id: str, notes?: str) -> ResolutionResult error: GovernanceError # List resolutions for a company list_resolutions(company_id: str) -> ResolutionListResult error: GovernanceError } service InvoiceService { version: "1.0.0" description: "Invoice management for companies" # List invoices for company list(fzc_id: str) -> InvoicesListResult error: InvoiceError # Create invoice create(fzc_id: str, data: CreateInvoiceData) -> InvoiceResult error: InvoiceError # Get invoice by ID get(invoice_id: str) -> InvoiceResult error: InvoiceError # Update invoice update(invoice_id: str, data: UpdateInvoiceData) -> InvoiceResult error: InvoiceError # Delete invoice delete(fzc_id: str, invoice_id: str) -> DeleteResult error: InvoiceError # Render the invoice email the sender is about to send (home#641 Slice B). # Read-only: renders through the same seam as send, persists nothing, mails # nothing. Gated as a PAIR (see tenant_boundary.rs / method_auth.rs), exactly # like send_draft: the CallerEqualsParam row gate binds sender_user_id to the # verified caller, and the handler re-checks invoice.sender_user_id == that # param. The gate alone proves WHO you are, never that you own THIS invoice — # dropping the handler check makes preview an IDOR over every invoice. preview_invoice_email(invoice_id: str, sender_user_id: str) -> PreviewInvoiceEmailResult error: InvoiceError } service KeypairService { version: "2.0.0" description: "Client-side keypair storage and verification — private key never leaves the browser" # Store client-generated keypair (public key + encrypted private key blob) store_client_keypair(entity_type: EntityType, entity_id: str, public_key: str, encrypted_private_key: str) -> KeypairResult error: KeypairError # Get encrypted private key blob (client decrypts locally) get_encrypted_private_key(entity_type: EntityType, entity_id: str) -> EncryptedKeyResult error: KeypairError # Verify signature using stored public key verify_signature(signature: str, document_hash: str, signer_id: str, signer_type: EntityType) -> VerifyResult error: VerifyError } service KycService { version: "1.0.0" description: "KYC verification management" # Start KYC verification session start_verification(resident_id: str, public_key: str, callback_url?: str) -> KycSessionResult error: KycError # Check KYC status check_status(session_id: str) -> KycStatusResult error: KycError # Handle iDenfy webhook handle_idenfy_webhook(data: IdenfyWebhookData) -> WebhookResult error: WebhookError } service MessageService { version: "1.0.0" description: "User and company messaging" # List user messages with filtering list_user_messages(user_id: str, category?: MessageCategory, is_read?: bool) -> MessagesListResult error: MessageError # Get user message get_user_message(user_id: str, message_id: str) -> MessageResult error: MessageError # Get unread count get_unread_count(user_id: str) -> UnreadCountResult error: MessageError # Mark message as read mark_as_read(user_id: str, message_id: str) -> MarkReadResult error: MessageError # Mark message as unread mark_as_unread(user_id: str, message_id: str) -> MarkReadResult error: MessageError # Mark all as read mark_all_read(user_id: str) -> MarkReadResult error: MessageError # Mark all as unread mark_all_unread(user_id: str) -> MarkReadResult error: MessageError # Delete message delete_message(user_id: str, message_id: str) -> DeleteResult error: MessageError # Create system message (admin) create_system_message(user_id: str, title: str, body: str, category?: MessageCategory, action_url?: str) -> MessageResult error: MessageError # List company messages list_company_messages(company_id: str) -> MessagesListResult error: MessageError # Create company message create_company_message(company_id: str, title: str, body: str, category?: MessageCategory) -> MessageResult error: MessageError # Mark company message as read mark_company_read(company_id: str, message_id: str) -> MarkReadResult error: MessageError # Mark company message as unread mark_company_unread(company_id: str, message_id: str) -> MarkReadResult error: MessageError # Mark all company messages read mark_all_company_read(company_id: str) -> MarkReadResult error: MessageError # Mark all company messages unread mark_all_company_unread(company_id: str) -> MarkReadResult error: MessageError } service PaymentService { version: "1.0.0" description: "Payment processing for residents and companies" # Get crypto wallet address for payment get_payment_wallet(resident_id: str, payment_type: PaymentType, amount: f64, currency: str) -> PaymentWalletResult error: PaymentError # Confirm payment confirm_payment(payment_id: str, sender_address?: str, transaction_hash?: str) -> PaymentConfirmResult error: PaymentError # Initiate ClickPesa payment initiate_clickpesa(entity_id: str, entity_type: EntityType, amount: f64, currency: str, description: str, payment_purpose: str, subscription_plan?: str) -> ClickPesaResult error: PaymentError # Handle ClickPesa webhook handle_payment_webhook(data: ClickPesaWebhookData) -> WebhookResult error: WebhookError # Renew a resident's subscription. Debits the caller's credit wallet for # the plan amount (with duration discount) and extends subscription_end_date # from max(current_end, now). Plan is one of: 1_month, 6_months, 1_year, # 2_years, 3_years. Wallet-only — use the ClickPesa/Stripe top-up flows to # add credits before renewing. initiate_renewal(user_id: str, plan: str) -> SubscriptionInfo error: PaymentError # Renew a company's subscription. Debits the caller's credit wallet for # monthly_fee × months(plan) × (1 - company_discount(plan)), extends the # company's subscription_end_date, and posts an inbox message. No setup fee. # Plan is a branding.toml plan code (e.g. 1_month, 6_months, 1_year, # 2_years, 3_years) — same vocabulary as resident renewal. initiate_company_renewal(user_id: str, company_id: str, plan: str) -> SubscriptionInfo error: PaymentError # Get subscription status for a user get_subscription_status(user_id: str) -> SubscriptionInfo error: PaymentError } service PricingService { version: "1.0.0" description: "Pricing calculation for residents and companies" # Calculate company registration pricing calculate_company_pricing(company_type: CompanyType, shareholder_count: i32, subscription_plan: CompanySubscriptionPlan, early_bird: bool) -> PricingResult error: PricingError # Get all pricing configurations (for display) get_all_pricing() -> AllPricingConfig error: PricingError # Get shareholder fee rate for a company type and count get_shareholder_fee_rate(company_type: CompanyType, shareholder_count: i32) -> ShareholderFeeRateResult error: PricingError } service PublicKeyHistoryService { version: "1.0.0" description: "Historical public key management for signature verification" # Get key history for an entity get_history(entity_type: EntityType, entity_id: str) -> PublicKeyHistoryResult error: KeypairError # Get specific historical key by version get_by_version(entity_type: EntityType, entity_id: str, version: i32) -> PublicKeyHistory error: KeypairError # Verify signature with historical key lookup verify_with_history(signature: str, entity_type: EntityType, entity_id: str) -> VerifyResult error: VerifyError } service RegistrationRequestService { version: "1.0.0" description: "Reseller-side registration request management" # Submit a new registration request submit_registration_request(reseller_id: str, request_type: RegistrationRequestType, client_email: str, client_name: str, kyc_session_id?: str, payment_id?: str, signed_contract_ids: [str], reseller_signature: str) -> RegistrationRequestResult error: RegistrationRequestError # Check status of a registration request get_registration_request_status(request_id: str) -> RegistrationRequestDetailResult error: RegistrationRequestError # List all requests for a reseller list_registration_requests(reseller_id: str) -> RegistrationRequestListResult error: RegistrationRequestError # Update a registration request (before approval, e.g., after needs_info) update_registration_request(request_id: str, client_email?: str, client_name?: str, kyc_session_id?: str, payment_id?: str, signed_contract_ids?: [str]) -> RegistrationRequestDetailResult error: RegistrationRequestError } service RegistrationService { version: "1.0.0" description: "Registration flow management" # Get registration status get_status(user_id: str, entity_type: EntityType) -> RegistrationStatusResult error: RegistrationError # Update registration status update_status(user_id: str, entity_type: EntityType, status: RegistrationFlowStatus) -> RegistrationStatusResult error: RegistrationError # Check if resident payment is complete check_payment_complete(user_id: str) -> PaymentCompleteResult error: RegistrationError # Check if user KYC is complete check_kyc_complete(user_id: str) -> KycCompleteResult error: RegistrationError # Activate resident if all requirements met activate_if_complete(user_id: str) -> ActivateResult error: RegistrationError # Confirm KYC (internal) confirm_kyc(user_id: str, resident_id: str, data: KycConfirmData) -> KycConfirmResult error: KycError } service ReportService { version: "1.0.0" description: "Financial report generation" # Generate balance sheet generate_balance_sheet(company_id: str, as_of_date: otime, generated_by: str) -> FinancialReportResult error: ReportError # Generate income statement generate_income_statement(company_id: str, period_start: otime, period_end: otime, generated_by: str) -> FinancialReportResult error: ReportError # Generate cash flow statement generate_cash_flow(company_id: str, period_start: otime, period_end: otime, generated_by: str) -> FinancialReportResult error: ReportError # Generate trial balance generate_trial_balance(company_id: str, as_of_date: otime, generated_by: str) -> FinancialReportResult error: ReportError # Get report get_report(report_id: str) -> FinancialReportDetailResult error: ReportError # List reports for a company list_reports(company_id: str, report_type?: FinancialReportType) -> FinancialReportListResult error: ReportError } service ResidentService { version: "1.0.0" description: "Digital resident management" # List residents with pagination list(user_id: str, page: i32, page_size: i32) -> ResidentsListResult error: ResidentError # Create a new resident create(user_id: str, data: CreateResidentData) -> ResidentResult error: ResidentError # Get resident by ID get(resident_id: str) -> ResidentResult error: ResidentError # Update resident update(resident_id: str, data: UpdateResidentData) -> ResidentResult error: ResidentError # Delete resident delete(resident_id: str) -> DeleteResult error: ResidentError # Update subscription update_subscription(resident_id: str, plan: str, auto_renewal: bool) -> SubscriptionResult error: ResidentError # Get resident card (public display info) get_card(user_id: str) -> ResidentCardResult error: ResidentError # home#473 — owner-only: flip the subject-side anonymity flag on the caller's OWN DR. # caller_user_id is server-injected; the target must satisfy resident.user_id == caller_user_id # (ownership, NOT act-as/proxy). Stamps anonymity_changed_at = now. set_anonymity(resident_id: str, enabled: bool, caller_user_id: str) -> ResidentResult error: ResidentError } service ShareholderService { version: "1.0.0" description: "Shareholder management, invites, and share transfers" # Invite someone to become a shareholder invite_shareholder(company_id: str, inviter_id: str, invitee_email: str, proposed_units: f64, message?: str) -> InviteResult error: ShareholderError # Accept shareholder invite accept_invite(invite_token: str, resident_id: str) -> AcceptInviteResult error: ShareholderError # Reject shareholder invite reject_invite(invite_token: str) -> RejectInviteResult error: ShareholderError # List pending invites for a company list_invites(company_id: str) -> InviteListResult error: ShareholderError # Initiate share transfer initiate_transfer(company_id: str, from_resident_id: str, to_resident_id: str, units: f64, reason?: str) -> TransferResult error: ShareholderError # Approve share transfer (by recipient) approve_transfer(transfer_id: str, to_signature: str) -> TransferResult error: ShareholderError # Complete share transfer complete_transfer(transfer_id: str) -> TransferResult error: ShareholderError # Cancel share transfer cancel_transfer(transfer_id: str) -> TransferResult error: ShareholderError # List share transfers for a company list_transfers(company_id: str, status?: ShareTransferStatus) -> TransferListResult error: ShareholderError # Initiate dividend distribution initiate_dividend(company_id: str, total_amount: f64, currency: str, source_account_id: str, distribution_date: otime, initiated_by: str) -> DividendResult error: ShareholderError # Execute dividend distribution execute_dividend(dividend_id: str) -> DividendResult error: ShareholderError # List dividend distributions for a company list_dividends(company_id: str) -> DividendListResult error: ShareholderError } service TaxService { version: "1.0.0" description: "Tax filing and compliance management" # Create tax filing create_filing(company_id: str, filing_type: TaxFilingPeriod, period_start: otime, period_end: otime) -> TaxFilingResult error: TaxError # Get tax filing get_filing(filing_id: str) -> TaxFilingDetailResult error: TaxError # Update tax filing (draft only) update_filing(filing_id: str, gross_revenue: f64, cost_of_goods_sold: f64, operating_expenses: f64) -> TaxFilingResult error: TaxError # Submit tax filing submit_filing(filing_id: str, submitted_by: str, password: str) -> TaxFilingResult error: TaxError # List tax filings for a company list_filings(company_id: str, status?: TaxFilingStatus) -> TaxFilingListResult error: TaxError # Get upcoming filing deadlines get_deadlines(company_id: str) -> TaxDeadlineResult error: TaxError # Generate tax document generate_document(filing_id: str, document_type: str) -> TaxDocumentResult error: TaxError # List tax documents for a filing list_documents(filing_id: str) -> TaxDocumentListResult error: TaxError } service TreasuryOperationsService { version: "1.0.0" description: "Fund transfers and currency exchange" # Initiate internal fund transfer initiate_transfer(from_account_id: str, to_account_id: str, amount: f64, currency: str, reference?: str, initiated_by: str) -> FundTransferResult error: TreasuryOperationsError # Initiate external fund transfer initiate_external_transfer(from_account_id: str, to_address: str, to_name: str, amount: f64, currency: str, reference?: str, initiated_by: str) -> FundTransferResult error: TreasuryOperationsError # Approve fund transfer approve_transfer(transfer_id: str, approved_by: str) -> FundTransferResult error: TreasuryOperationsError # Cancel fund transfer cancel_transfer(transfer_id: str) -> FundTransferResult error: TreasuryOperationsError # List fund transfers for an account list_transfers(account_id: str, status?: FundTransferStatus) -> FundTransferListResult error: TreasuryOperationsError # Get exchange rate quote get_exchange_quote(from_currency: str, to_currency: str, amount: f64) -> ExchangeQuoteResult error: TreasuryOperationsError # Execute currency exchange execute_exchange(owner_type: EntityType, owner_id: str, from_account_id: str, to_account_id: str, from_amount: f64, from_currency: str, exchange_rate: f64, initiated_by: str) -> ExchangeResult error: TreasuryOperationsError # List exchange transactions list_exchanges(owner_type: EntityType, owner_id: str) -> ExchangeListResult error: TreasuryOperationsError } service TreasuryService { version: "1.0.0" description: "Treasury account and transaction management" # List company treasury accounts list_company_accounts(company_id: str) -> TreasuryAccountsResult error: TreasuryError # Get company treasury account get_company_account(company_id: str, account_id: str) -> TreasuryAccountResult error: TreasuryError # List company treasury transactions list_company_transactions(company_id: str, account_id: str) -> TreasuryTransactionsResult error: TreasuryError # Create company treasury account create_company_account(company_id: str, data: CreateTreasuryAccountData) -> TreasuryAccountResult error: TreasuryError # Update company treasury account update_company_account(company_id: str, account_id: str, data: UpdateTreasuryAccountData) -> TreasuryAccountResult error: TreasuryError # List resident treasury accounts list_resident_accounts(resident_id: str) -> TreasuryAccountsResult error: TreasuryError # Get resident treasury account get_resident_account(resident_id: str, account_id: str) -> TreasuryAccountResult error: TreasuryError # List resident treasury transactions list_resident_transactions(resident_id: str, account_id: str) -> TreasuryTransactionsResult error: TreasuryError # Create resident treasury account create_resident_account(resident_id: str, data: CreateTreasuryAccountData) -> TreasuryAccountResult error: TreasuryError # Update resident treasury account update_resident_account(resident_id: str, account_id: str, data: UpdateTreasuryAccountData) -> TreasuryAccountResult error: TreasuryError } service UserAuditLogService { version: "1.0.0" description: "Typed append-only audit trail (FREEZONE.md principle 4). Session 49 seed." # Append a new audit row. No update, no delete — audit is append-only by construction. # Callers set allowed=true on success, allowed=false on denial/validation-fail. # `context` is caller-serialized JSON (correlation_id, request origin, redacted reason…). log(subject_id: str, action: AuditAction, target_ref: str, target_type: AuditTargetType, allowed: bool, context: str) -> UserAuditLogResult error: UserAuditLogError # List rows for a subject ("what did this actor do?"). Newest first, capped at `limit`. # limit <= 0 means server default (100). list_for_subject(subject_id: str, limit: i32) -> UserAuditLogListResult error: UserAuditLogError # List rows for a target ("who touched this entity?"). Newest first, capped at `limit`. # limit <= 0 means server default (100). list_for_target(target_ref: str, limit: i32) -> UserAuditLogListResult error: UserAuditLogError } service VerificationService { version: "1.0.0" description: "Tiered verification for individuals and companies" # Verify individual (Tier 1: anonymous, Tier 2: member, Tier 3: via link) verify_individual(resident_id: str, requester_tier: i32, link_token?: str) -> IndividualVerificationResult error: VerificationError # Verify company (Tier 1: anonymous, Tier 2: member, Tier 3: via link) verify_company(company_id: str, requester_tier: i32, link_token?: str) -> CompanyVerificationResult error: VerificationError # Create verification link create_verification_link(entity_type: EntityType, entity_id: str, label?: str, expires_in_hours: i32, single_use: bool) -> CreateLinkResult error: LinkError # Revoke verification link revoke_verification_link(link_id: str) -> RevokeResult error: LinkError # List verification links for user list_verification_links(user_id: str) -> VerificationLinksResult error: LinkError # Delete inactive verification links delete_inactive_links(user_id: str) -> DeleteLinksResult error: LinkError # Verify by token (public endpoint) verify_by_token(token: str) -> VerifyByTokenResult error: VerificationError # home#472 — owner-only: produce the server-signed opaque verify URL for the # caller's own entity (dashboard/cert QR). caller_user_id is server-injected; # entity_type is "individual" or "company". Legacy rows (no opaque id) -> None. public_verify_url(entity_type: str, id: str, caller_user_id: str) -> PublicVerifyUrlResult error: VerificationError } # ─────────────────────────────────────────────────────────────────────────────── # ENUMS - String literal union types # ─────────────────────────────────────────────────────────────────────────────── # AI provider for user AI agent AiProvider = "groq" | "openrouter" | "sambanova" | "deepinfra" # AuditAction — kinds of audited actions. Session 49 seeds 4 action variants (the mutation # sites wired this session) plus an "other" fallback for generic UserAuditLogService.log # callers. Session 50 adds view_document for can_view-consumer audit rows (paired with # a typed VisibilityDecision sub-record). Session 53 adds appoint_director + remove_director # for Director CompanyRole lifecycle (M&A Art. 17/18). Extend when new audited mutation # sites are wired. # Deliberately distinct from DocumentKind: DocumentKind discriminates document *entities*; # AuditAction discriminates *actions* taken against any kind of target. # # Session 54 adds "grant_signing_authority" / "revoke_signing_authority" for delegated # SigningAuthority lifecycle (Board Resolution-granted signing per M&A Art. 3 + Alex Q1). # Director-auto signing is NOT audited here — the underlying Director CompanyRole # appointment already produces an "appoint_director" audit row (session 53), and there is # no separate stored row to grant/revoke. See decisions/012-signing-authority.md (virtual # director-auto scope — B.5 catch). # # Session 55 adds "governance_step" (single variant covering all 5 due-process steps; # the specific step is stored on GovernanceAction.kind, not duplicated on the audit # enum — keeps the enum tight, matches D-013 audit-shape option a) and "enforce_decision" # (separate variant because enforcement is the closure side-effect, not a state-chain # step — pairing it with the closure-bearing handler gives per-step filter granularity # exactly where it's load-bearing). # # Session 57 adds "revoke_api_key" + "rotate_api_key" for personal ApiKey lifecycle # (list/revoke/rotate handlers per #311). Generation is not audited here because # `apikeyservice.generate` predates the audit primitive and exists on an unrelated # authorization path (password re-confirm at generate time); revoke + rotate are the # post-creation mutations worth observing. See decisions/015-personal-api-key-mgmt.md. # Session 59 adds "ai_query_debit" for the idempotent post-debit write on aibroker # billing rows. See decisions/017-ai-billing-freezone-side.md. # Session 63 adds "kyc_artifact_submit" for the partner-track π-4 delegated-KYC # attestation write. Fires once per KycArtifactAttestation row creation from the # partner-aware submit wrapper. See decisions/021-partner-track-kyc-artifact.md. # Session 64 adds "kyc_artifact_view" for the partner-track π-5 admin read-only # audit-view handler. Distinct from kyc_artifact_submit: submit is the write-side # event (partner shipped an artifact), view is the read-side event (an admin # decrypted the PII). Separating the variants lets audit queries distinguish # "artifact ingested" from "admin examined". See decisions/022-partner-track-admin-audit-view.md. # Session 65 adds the four MCP Tier-6 approval-flow variants. approved/rejected # are the owner's decision; executed is the system running the tool after approve; # expired is the sweep auto-closing a Pending row whose expires_at elapsed. Four # distinct variants (not one mcp_approval_decision) so the audit query shape # "how many Tier-6 calls ever executed vs how many approvals were granted" # reduces to a one-field filter. See decisions/023-mcp-tier6-approval.md. AuditAction = "sign_contract" | "renew_subscription" | "verify_kyc" | "grant_bonus" | "view_document" | "appoint_director" | "remove_director" | "grant_signing_authority" | "revoke_signing_authority" | "governance_step" | "enforce_decision" | "revoke_api_key" | "rotate_api_key" | "ai_query_debit" | "partner_registration_request_submit" | "partner_registration_request_approve" | "kyc_artifact_submit" | "kyc_artifact_view" | "kyc_personal_data_view" | "mcp_approval_approved" | "mcp_approval_rejected" | "mcp_approval_executed" | "mcp_approval_expired" | "mcp_approval_admin_approved" | "mcp_approval_admin_rejected" | "mcp_approval_admin_viewed" | "void_registration" | "view_logs" | "other" # AuditTargetType — kinds of entities an audited action targets. Deliberately broader than # DocumentKind: audit targets include non-document entities (user, wallet, kyc_session). # Session 50 adds 3 DocumentKind-aligned variants (invoice, business_document, share_transfer) # for can_view-consumer audit rows — "contract" was already present from session 49. These # carry an orthogonal VisibilityDecision sub-record for the visibility-specific fields # (audience_sid, matched_role_sids, classification_intersected, as_of_epoch, …). # Session 53 adds "company_role" so appoint_director / remove_director can target the # CompanyRole row by sid (rather than the company or subject by proxy). # Session 54 adds "signing_authority" so designate_signatory / revoke_signatory can target # the SigningAuthority row by sid. # Session 55 adds "governance_action" so governance_step / enforce_decision can target # the GovernanceAction row by sid. # Session 57 adds "api_key" so revoke_api_key / rotate_api_key can target the ApiKey # row by sid. # Session 59 adds "ai_call" so ai_query_debit can target a specific upstream AI query; # target_ref carries the external call_id so audit rows cross-link to aibroker's log. # Session 63 adds "kyc_artifact" so kyc_artifact_submit targets the attestation row # (never the payload row — the audit chain follows the immutable side). See D-021. # Session 65 adds "mcp_approval" so the four mcp_approval_* variants target the # McpApproval row by sid. Target is always the approval, never the underlying # tool's output entity (forensics want the approval chain to be traceable as one # unit). See decisions/023-mcp-tier6-approval.md. AuditTargetType = "contract" | "invoice" | "business_document" | "share_transfer" | "subscription" | "kyc_session" | "user" | "wallet" | "company" | "company_role" | "signing_authority" | "governance_action" | "api_key" | "ai_call" | "registration_request" | "kyc_artifact" | "kyc_personal_data" | "mcp_approval" | "other" # Business document type BusinessDocumentType = "certificate_of_incorporation" | "business_license" | "memorandum" | "articles" | "resolution" | "other" # Company role kind — who holds what position in a company/coop. # M1 ships: shareholder (FZC investor), founding_member / platinum_member (coop tiers), # founder (creator marker, always written alongside the creator's first substantive role), # director (company governance role; M&A Art. 17/18 appointment/removal by shareholders' # Ordinary Resolution — Q1 pulled forward from M2 per Alex 2026-04-22). # M2 will add: "officer" | "liquidator" — additive, no schema break. CompanyRoleKind = "shareholder" | "founding_member" | "platinum_member" | "founder" | "director" # Free Zone Company status CompanyStatus = "draft" | "pending_signatures" | "active" | "inactive" | "suspended" | "voided" # Company subscription plan for recurring fees CompanySubscriptionPlan = "monthly" | "one_year" | "two_years" # Company types available in the freezone CompanyType = "single_fzc" | "startup_fzc" | "growth_fzc" | "global_fzc" | "coop_standard_fzc" | "coop_platinum_fzc" # Contract status ContractStatus = "draft" | "pending_signature" | "active" | "expired" | "archived" # Credit transaction status CreditTransactionStatus = "pending" | "completed" | "failed" # Credit transaction type CreditTransactionType = "top_up" | "deduction" | "refund" | "bonus" | "expiry" # DecisionOutcome — the ruling in an `issue_decision` call. Re-used for both original- # decision rows (parent is a review) AND appeal-ruling rows (parent is an appeal). # Semantics discriminate by parent: # - On a review-parented decision: the outcome applies to the member (dismiss = # no discipline; warning/probation/suspension = intermediate; remove = expulsion). # - On an appeal-parented decision: `dismiss` means "appeal denied → original stands # → enforcement allowed"; any non-dismiss outcome means "appeal granted → original # set aside → enforcement blocked permanently" (M1 has no automatic re-appeal or # re-ruling flow; restoration is manual if needed). # The re-use keeps the enum tight and lets one handler (`issue_decision`) cover both # original rulings and appeal rulings. Principle-3 pure: outcomes are never mutated; # changes always go via new rows. DecisionOutcome = "dismiss" | "warning" | "probation" | "suspension" | "remove" # Dividend distribution status DividendStatus = "proposed" | "approved" | "processing" | "completed" | "cancelled" # DocumentKind — discriminates which OSIS entity a DocumentAudience row references. # Added in session 48 alongside DocumentAudience. Extend when new document-bearing entity # types ship; a DocumentAudience row's document_type must match its document_ref's entity. DocumentKind = "contract" | "invoice" | "inbox_message" | "business_document" | "proposal" | "resolution" | "tax_filing" | "financial_report" | "expense" | "share_transfer" | "dividend" # Document types for signing DocumentType = "contract" | "agreement" | "invoice" | "certificate" | "other" # Entity draft status (multi-sig creation flow) EntityDraftStatus = "draft" | "pending_signatures" | "all_signed" | "paid" | "active" | "cancelled" | "expired" # ═══════════════════════════════════════════════════════════════════════════════ # ZANZIBAR DIGITAL FREEZONE - OSIS Schema v2.0 # ═══════════════════════════════════════════════════════════════════════════════ # # A complete schema-driven system for digital residency and company management. # Features: # - Type-safe enums for all categorical data # - Full-text search on relevant fields # - Declarative business rules # - Cryptographic identity with key versioning # # ═══════════════════════════════════════════════════════════════════════════════ # ─────────────────────────────────────────────────────────────────────────────── # ENUMS - Type-safe categorical values # ─────────────────────────────────────────────────────────────────────────────── # Entity type for polymorphic references (resident or company) EntityType = "resident" | "company" # Exchange transaction status ExchangeStatus = "pending" | "processing" | "completed" | "failed" | "expired" # ExecutorRole — who may act at each due-process step. Alex Q3 enumerated 9 options. # The coop picks ONE per step at formation (stored in # FreeZoneCompany.due_process_executors); absent that config, handlers fall back to # branding.toml [governance.due_process.default_executors]. Step 5 "arbitration" # is RECORDING-ONLY in M1 per Alex Q9 — handler stores the filing/outcome reference # but does not implement arbitration mechanics. Full on-platform arbitration is M5+. ExecutorRole = "ethics_committee" | "governance_board" | "review_panel" | "rotating_impartial_committee" | "external_mediator" | "random_member_jury" | "general_assembly" | "internal_appeals_committee" | "arbitration" # Expense status ExpenseStatus = "draft" | "submitted" | "approved" | "rejected" # Financial report type FinancialReportType = "balance_sheet" | "income_statement" | "cash_flow" | "trial_balance" # Form field type for dynamic form schemas FormFieldType = "text" | "textarea" | "dropdown" | "number" | "checkbox" | "multi_select" # Fund transfer status FundTransferStatus = "pending" | "processing" | "completed" | "failed" | "cancelled" # GovernanceActionKind — the 5 fixed due-process steps from Coop Bylaws (session 55, # Alex Q3 resolved 2026-04-22, #308#22569). One GovernanceAction row per step; rows # chain via parent_action_sid: notice → response → review → decision → appeal. An # appeal RULING is itself a `decision` row whose parent points at the appeal row — # the parent's kind discriminates which executor set applies (Step-4 for review- # parented decisions; Step-5 for appeal-parented decisions). See decisions/013. GovernanceActionKind = "notice" | "response" | "review" | "decision" | "appeal" # Invoice status InvoiceStatus = "draft" | "sent" | "paid" | "overdue" | "cancelled" | "final" # External-company KYB verification status (home#475). Manual verification first. KybStatus = "pending" | "in_progress" | "approved" | "rejected" # KYC verification status # needs_review: provider verdict was SUSPECTED and the instance requires manual # admin sign-off (branding [features.kyc] suspected_requires_review). Resident # stays Pending until an admin approves/rejects via kycservice.approve/reject. KycStatus = "pending" | "in_progress" | "completed" | "failed" | "expired" | "needs_review" # MCP approval status enum. # Session 65 adds "approved_executed" as a fifth terminal state: approved is the # owner's decision *before* the tool actually runs; approved_executed is after # successful dispatch. Splitting the two states makes the approve handler # crash-safe — a crash between status-flip and tool dispatch leaves the row in # Approved (not ApprovedExecuted), and a re-run of the approve handler resumes # from Approved without a second decision. D-011 closure primitive applies: # Pending → Approved → ApprovedExecuted | execution_error set on row. # See decisions/023-mcp-tier6-approval.md. McpApprovalStatus = "pending" | "approved" | "approved_executed" | "rejected" | "expired" # Message categories MessageCategory = "account" | "registration" | "security" | "kyc" | "payment" | "company" | "resident" | "general" # Message types MessageType = "system" | "notification" | "alert" # ─────────────────────────────────────────────────────────────────────────────── # ONBOARDING TYPES - Progressive unlock tiers # ─────────────────────────────────────────────────────────────────────────────── # Step status in the onboarding journey OnboardingStepStatus = "locked" | "available" | "completed" # Payment method enum PaymentMethod = "crypto" | "clickpesa" | "bank_transfer" | "card" # Payment status PaymentStatus = "pending" | "confirming" | "confirmed" | "failed" | "expired" # Payment types PaymentType = "bitcoin" | "ethereum" | "clickpesa" | "stripe" | "bank_transfer" # Proposal category ProposalCategory = "financial" | "strategic" | "operational" | "governance" | "personnel" # Proposal status ProposalStatus = "draft" | "active" | "passed" | "rejected" | "expired" | "executed" # Proxy role — what kind of act-as authority the proxy holds (home#475). ProxyRole = "proxy" | "admin_proxy" | "signer" # Proxy status — operational kill-switch, ANDed with the valid_from/valid_to window. ProxyStatus = "active" | "revoked" # Registration contract categories RegistrationContractCategory = "terms_of_service" | "privacy_policy" | "digital_residency" | "company_formation" # Registration flow status RegistrationFlowStatus = "identity_pending" | "identity_complete" | "personal_info_complete" | "payment_pending" | "payment_complete" | "kyc_pending" | "kyc_complete" | "registration_complete" # Registration request status for reseller-submitted registrations RegistrationRequestStatus = "pending" | "under_review" | "approved" | "rejected" | "needs_info" # Registration request type (resident or company) RegistrationRequestType = "resident" | "company" # Digital Resident provenance — the CLASS of how the DR entered the system (home#475). # Decoupled from `partner_id` (the reseller-ownership axis): `source` records the class, # the specific partner identity lives in `partner_id` -> PartnerConfig. No per-partner variant # (e.g. "binance") — that would grow the enum forever; `partner` covers all integrators. ResidentSource = "direct" | "admin" | "partner" # Digital Resident status ResidentStatus = "pending" | "active" | "suspended" | "expired" # Digital Resident type — how the DR is used. External companies are a DR type, # not a parallel actor (home#475). `personal` is a normal individual; # `external_company` is a company outside the freezone represented BY a DR (user_id=None, # reached only via a Proxy act-as); `freezone_company` is a company incorporated inside; # `partner_created` is auto-created via a partner flow. Optional/None on legacy rows. ResidentType = "personal" | "external_company" | "freezone_company" | "partner_created" # Resolution kind (session 56 / D-014). Drives threshold selection from # branding.toml [governance.resolution_thresholds] at create_proposal time. # Ordinary = simple majority (> 50%); Special = super-majority (>= 75%); # Board = board-of-directors resolution (not a shareholder vote; consumed by # designate_signatory to authorise a delegated SigningAuthority grant). ResolutionKind = "ordinary" | "special" | "board" # AML/KYC risk tier (session 67, D-025 forward-compat per D-024 invariant 6). # Populated by M2 ComplianceReview (#345) from iDenfy AML risk score per Compliance # WP §3.B (70-100 Low / 41-69 Medium / 0-40 High; PEP is a separate screening flag). # Drives scheduled-refresh cadence per WP §3.C (Low=annual, Medium=biannual, High/PEP=quarterly). # No M1 logic reads this; ships Option-None on every row so M2 doesn't backfill. RiskTier = "low" | "medium" | "high" | "pep" # Share transfer status ShareTransferStatus = "pending" | "approved" | "rejected" | "completed" | "cancelled" # Shareholder invite status ShareholderInviteStatus = "pending" | "accepted" | "rejected" | "expired" # ─────────────────────────────────────────────────────────────────────────────── # SUBSCRIPTION TYPES - Resident subscription lifecycle # ─────────────────────────────────────────────────────────────────────────────── # Subscription status (computed by backend from dates) SubscriptionStatus = "none" | "active" | "expiring" | "suspended" # Tax filing period type TaxFilingPeriod = "monthly" | "quarterly" | "annual" # Tax filing status TaxFilingStatus = "draft" | "submitted" | "accepted" | "rejected" | "amended" # Treasury account kind TreasuryAccountKind = "crypto_wallet" | "bank_account" # Treasury transaction status TreasuryTransactionStatus = "pending" | "confirmed" | "failed" # Treasury transaction type TreasuryTransactionType = "credit" | "debit" | "transfer" # Verification link status VerificationLinkStatus = "active" | "expired" | "used" | "revoked" # Vote value VoteValue = "for" | "against" | "abstain" # Voting model for companies VotingModel = "standard" | "cooperative"