rspace-online/modules/rbnb/schemas.ts

277 lines
7.1 KiB
TypeScript

/**
* rBnb Automerge document schemas.
*
* Community hospitality — trust-based space sharing and couch surfing.
* Granularity: one Automerge document per space (listings + stays + endorsements).
* DocId format: {space}:bnb:listings
*/
import type { DocSchema } from '../../shared/local-first/document';
// ── Economy model ──
export type EconomyModel = 'gift' | 'suggested' | 'fixed' | 'sliding_scale' | 'exchange';
// ── Listing types ──
export type ListingType =
| 'couch'
| 'room'
| 'apartment'
| 'cabin'
| 'tent_site'
| 'land'
| 'studio'
| 'loft'
| 'house'
| 'other';
// ── Stay request status ──
export type StayStatus =
| 'pending'
| 'accepted'
| 'declined'
| 'cancelled'
| 'completed'
| 'endorsed';
// ── Endorsement visibility ──
export type EndorsementVisibility = 'public' | 'private' | 'community';
// ── Core types ──
export interface AvailabilityWindow {
id: string;
listingId: string;
startDate: number; // epoch ms (start of day)
endDate: number; // epoch ms (end of day)
status: 'available' | 'blocked' | 'tentative';
notes: string | null;
createdAt: number;
}
export interface Listing {
id: string;
hostDid: string; // DID of the host
hostName: string;
title: string;
description: string;
type: ListingType;
economy: EconomyModel;
// Pricing (relevant for non-gift economies)
suggestedAmount: number | null; // suggested/fixed price per night
currency: string | null; // e.g. 'USD', 'EUR', or null for gift
slidingMin: number | null; // sliding scale minimum
slidingMax: number | null; // sliding scale maximum
exchangeDescription: string | null; // what the host wants in exchange
// Location
locationName: string;
locationLat: number | null;
locationLng: number | null;
locationGranularity: string | null; // 'city', 'neighborhood', 'address'
// Capacity & details
guestCapacity: number;
bedroomCount: number | null;
bedCount: number | null;
bathroomCount: number | null;
amenities: string[]; // e.g. ['wifi', 'kitchen', 'laundry', 'parking']
houseRules: string[]; // e.g. ['no_smoking', 'quiet_hours', 'shoes_off']
photos: string[]; // asset IDs or URLs
coverPhoto: string | null;
// Trust & auto-accept
trustThreshold: number | null; // 0-100, rNetwork trust score for auto-accept
instantAccept: boolean; // if true + trust met → auto-accept requests
// Metadata
isActive: boolean;
createdAt: number;
updatedAt: number;
}
export interface StayMessage {
id: string;
senderDid: string;
senderName: string;
body: string;
sentAt: number;
}
export interface StayRequest {
id: string;
listingId: string;
guestDid: string;
guestName: string;
hostDid: string;
// Dates
checkIn: number; // epoch ms
checkOut: number; // epoch ms
guestCount: number;
// Status flow: pending → accepted/declined → completed → endorsed
status: StayStatus;
// Messages embedded in the CRDT (conversation lives in the request)
messages: StayMessage[];
// Contribution (for non-gift economies)
offeredAmount: number | null;
offeredCurrency: string | null;
offeredExchange: string | null;
// Timestamps
requestedAt: number;
respondedAt: number | null;
completedAt: number | null;
cancelledAt: number | null;
}
export interface Endorsement {
id: string;
stayId: string;
listingId: string;
// Who wrote it and about whom
authorDid: string;
authorName: string;
subjectDid: string; // the person being endorsed (host or guest)
subjectName: string;
direction: 'guest_to_host' | 'host_to_guest';
// Content
body: string;
rating: number | null; // 1-5, optional (endorsements > ratings)
tags: string[]; // e.g. ['welcoming', 'clean', 'great_conversation']
visibility: EndorsementVisibility;
// Trust integration — feeds into rNetwork trust graph
trustWeight: number; // 0-1, how much this endorsement affects trust score
createdAt: number;
}
export interface SpaceConfig {
defaultEconomy: EconomyModel;
defaultTrustThreshold: number; // 0-100
amenityCatalog: string[]; // available amenities for this community
houseRuleCatalog: string[]; // available house rules
endorsementTagCatalog: string[]; // available endorsement tags
requireEndorsement: boolean; // whether both parties must endorse after stay
maxStayDays: number; // maximum stay length in days
}
// ── Top-level document ──
export interface BnbDoc {
meta: {
module: string;
collection: string;
version: number;
spaceSlug: string;
createdAt: number;
};
config: SpaceConfig;
listings: Record<string, Listing>;
availability: Record<string, AvailabilityWindow>;
stays: Record<string, StayRequest>;
endorsements: Record<string, Endorsement>;
}
// ── Schema registration ──
export const DEFAULT_AMENITIES = [
'wifi', 'kitchen', 'laundry', 'parking', 'garden', 'workspace',
'heating', 'air_conditioning', 'hot_water', 'towels', 'linens',
'pets_welcome', 'wheelchair_accessible', 'bike_storage',
];
export const DEFAULT_HOUSE_RULES = [
'no_smoking', 'quiet_hours', 'shoes_off', 'no_parties',
'clean_up_after', 'no_pets', 'check_in_by_10pm',
];
export const DEFAULT_ENDORSEMENT_TAGS = [
'welcoming', 'clean', 'great_conversation', 'respectful',
'generous', 'good_communication', 'safe_space', 'cozy',
'well_located', 'quiet', 'fun', 'helpful',
];
const DEFAULT_CONFIG: SpaceConfig = {
defaultEconomy: 'gift',
defaultTrustThreshold: 30,
amenityCatalog: DEFAULT_AMENITIES,
houseRuleCatalog: DEFAULT_HOUSE_RULES,
endorsementTagCatalog: DEFAULT_ENDORSEMENT_TAGS,
requireEndorsement: false,
maxStayDays: 30,
};
export const bnbSchema: DocSchema<BnbDoc> = {
module: 'bnb',
collection: 'listings',
version: 1,
init: (): BnbDoc => ({
meta: {
module: 'bnb',
collection: 'listings',
version: 1,
spaceSlug: '',
createdAt: Date.now(),
},
config: { ...DEFAULT_CONFIG },
listings: {},
availability: {},
stays: {},
endorsements: {},
}),
};
// ── Helpers ──
export function bnbDocId(space: string) {
return `${space}:bnb:listings` as const;
}
/** Economy model display labels */
export const ECONOMY_LABELS: Record<EconomyModel, string> = {
gift: 'Gift Economy',
suggested: 'Suggested Contribution',
fixed: 'Fixed Price',
sliding_scale: 'Sliding Scale',
exchange: 'Skill/Service Exchange',
};
/** Listing type display labels */
export const LISTING_TYPE_LABELS: Record<ListingType, string> = {
couch: 'Couch',
room: 'Private Room',
apartment: 'Apartment',
cabin: 'Cabin',
tent_site: 'Tent Site',
land: 'Land',
studio: 'Studio',
loft: 'Loft',
house: 'House',
other: 'Other',
};
/** Listing type icons */
export const LISTING_TYPE_ICONS: Record<ListingType, string> = {
couch: '\u{1F6CB}', // couch
room: '\u{1F6CF}', // bed
apartment: '\u{1F3E2}', // building
cabin: '\u{1F3E1}', // house
tent_site: '\u{26FA}', // tent
land: '\u{1F333}', // tree
studio: '\u{1F3A8}', // palette
loft: '\u{1F3D7}', // building construction
house: '\u{1F3E0}', // house
other: '\u{1F3E8}', // hotel
};