# PRD — everspace (Everyspace)

Evidence date: 2026-10-15.

Source: `git@bitbucket.org:overdrive/everspace.git`, `master` at `c784099` (2025-11-05, "Request changes").

No prior PRD exists in the source repository or in Silicon Ark. Requirements below are recovered from routes, methods, schemas, jobs, and `README.md`. Provenance is labelled. Secret values found in the repository are not reproduced.

## Product purpose

**Documented (UI copy).** Search and listing pages describe Everyspace as a way to search for and contact the crew about film-friendly locations in South Africa: homes, offices, vehicles, and photography studios (`imports/routes/listings/search.jsx`, `imports/routes/general/routes.jsx`).

**Implemented.** A logged-in filmmaker can request a paid booking of a published listing. The host can accept or decline. An admin can approve a listing before it is searchable. Messaging, reviews, receipts, invoices, and a location agreement PDF sit around that booking.

**Inferred.** The product is a two-sided marketplace: hosts supply spaces, filmmakers rent them, Everyspace charges service fees and moves card payments through Peach Payments.

**Unknown / Requires Confirmation:** the legal entity, the countries besides South Africa, and whether non-film activities (weddings, meetings, parties) are in the commercial offer or only in the activity taxonomy. The taxonomy includes those activities (`imports/api/Activities/both/enums.ts`).

## Users and roles

| Actor | How the system recognises them | Status |
|---|---|---|
| Visitor | No `Meteor.userId()` | **Implemented.** Can open home, search (if a map centre is present), listing detail, legal, contact |
| Account holder | Meteor user, password and/or Google and/or Facebook | **Implemented** |
| Filmmaker | Default acting mode `userActingAs = "filmmaker"` in `localStorage` | **Implemented** client mode, not a role |
| Host (acting mode) | `userActingAs = "host"` after the user has listings, or when they switch in the dashboard | **Implemented** client mode |
| Host (role) | `alanning:roles` role `host` in `Roles.GLOBAL_GROUP`, toggled by `toggleUserIsHost` | **Implemented.** The method body does not check the caller |
| Listing editor | Role `editor` scoped to the listing id. Granted in `listing.create` | **Implemented** |
| Admin | Role `admin` in `Roles.GLOBAL_GROUP`. `/admin` routes redirect others to `/` | **Implemented** |
| Bootstrap admins | On startup, users for `dean@codeswop.com`, `regardt@codeswop.com`, and `hello@everyspace.co.za` are created if missing and given `admin`. Password is hardcoded | **Implemented.** **Requires confirmation** that this bootstrap is still wanted |
| Banned user | `user.banned === true` fails `Accounts.validateLoginAttempt` | **Implemented** |
| Deploy operator | GitLab CI, Docker, PM2, shell `scp` notes | **Documented** in README and CI. No in-app UI |

Password login is refused until `emails[].address` equals `profile.email` and `verified` is true. Google and Facebook logins are not subject to that same check (`imports/api/Users/server/init/users.js`). Banned users are refused for any login type that reaches the hook.

## Systems the product talks to

| System | Direction | Purpose | Evidence |
|---|---|---|---|
| Browser | In | Meteor DDP, React pages | Application structure |
| MongoDB | Out | All product data | `MONGO_URL`, collections |
| Peach Payments (OPPWA `https://oppwa.com`) | Out | Card pre-authorisation, capture, reversal, refund, card registration. Currency ZAR | `PeachPaymentsService.js` |
| SMTP | Out | Meteor email and Handlebars templates | `MAIL_URL`, `private/email-templates` |
| Twilio | Out | SMS helper `sendSMS` | `imports/api/Users/server/init/sms.js` |
| Intercom | Out | Contact sync and product events. Client `window.Intercom("update")` on navigation | `EveryspaceIntercomClient`, `router.js` |
| ActiveCampaign | Out | Contact fields and booking counts. Host documented as `filmspace.api-us1.com` in README and `pm2.json` | `imports/api/ActiveCampaign` |
| Object storage via AWS SDK | Out | Image storage client, region `af-south-1` | `digital-ocean-spaces-client.js` |
| Google OAuth, Facebook OAuth | Out | Social login | `authentication_services.js` |
| Google reCAPTCHA | Out | `recaptchaVerify` | `imports/api/Users/server/methods/users.js` |
| Google Tag Manager | Out | `UA-158596565-1` page views | `imports/routes/router.js` |
| Facebook pixel | Out | `fbq("track", "PageView")` | `router.js` |
| Google Maps | In the client | Listing address map component | `ListingAddress` route, `GoogleMap` |
| Chromium / Puppeteer | Local | PDF generation inside the server process | `Dockerfile`, `puppeteer` dependency |

Authentication material for Peach, Twilio, Google, Facebook, and reCAPTCHA is committed in source. ActiveCampaign, SMTP, Mongo, and object-storage secrets are committed in `README.md`, `pm2.json`, and `package.json` (`start-dev-db`). Values are omitted here. **Requires confirmation** that they have been rotated.

## Major workflows

### WF-1 — Arrive and search

1. `/` mounts `EveryspaceLandingPage`.
2. `/search/:page?` redirects to `/` unless query param `center` is set.
3. Search method `listings.search` filters listings with `status: "active"` by space tag, activity, attendee range or exact attendees, types, amenities, styles, price, location, and optional custom area.
4. Public location shortcuts come from `locations` where `isPublic` is true. Landing shortcuts also require `isLanding`.
5. Featured block publishes up to 6 listings with `isFeatured`, `status: "active"`, and `published: true`.

Provenance: **Implemented.**

### WF-2 — Create an account and sign in

Routes: `/create-account`, `/register`, `/login`, `/login/someone-else`, `/request-reset-password`, `/reset-password/:token/:userId`, `/verify-email/:token`, `/verify-your-email`, `/verify-mobile/:token`.

- Logged-in users hitting create, register, or login are redirected (`redirectIfLoggedIn`). The default redirect name is `UserProfile`.
- `/login` shows `RememberedPage` when `localStorage["Meteor.rememberMe"]` is set. `/login/someone-else` clears that key and redirects to `/login`.
- Account schema: first name minimum 3 characters, last name, email, password and confirm password must match, mobile minimum 10 characters, default country code `27`, newsletter default true, optional company, website, about, VAT number, city, filmmaker types, reCAPTCHA token.
- On create, a `newsletter` document is inserted for the profile email. Verification email is sent. Intercom contact is created on login if missing.
- Email verification success calls `user.intercom.add` and goes to `/`. Failure is logged in the browser console. The verify route does not mount an error page.
- Password reset email is sent only when a user with that email exists. The method does not show whether the address was found (the caller behaviour depends on the page; the method itself returns without an error when the user is missing).

Provenance: **Implemented.**

### WF-3 — Create and publish a listing

1. `/listing/create` requires login, calls `listing.create`, then opens `/listing/:listingId/address`.
2. `listing.create` inserts `status: "inactive"`, `published: false`, `createdBy` = current user, and grants that user the `editor` role for that listing id.
3. Leaving the address step calls `listing.deleteIfEmpty`.
4. Later steps are a long FlowRouter wizard (category, detail by category family, parking, amenities, restrictions, photos, style forms, pricing, additional fees, rules, calendar, activities). A step is mounted only if the user is `editor` on that listing or a global `admin`. Otherwise the user is sent home.
5. Which style forms apply is data-driven from `forms_for_categories`, keyed by subcategory. Admin edits that mapping at `/admin/listing-forms`.
6. Possible statuses, from the method comment and dashboard filters: `inactive`, `in-review`, `active`.
7. While status is `in-review`, only an admin may call `listing.status.set`.
8. Admin setting `active` and `published: true` records Intercom `admin_approved_listing` and, once, sets the owner's `_ownerConfirmedActivation` to false until `listing.ownerConfirmActivation`.
9. Admin setting `inactive` and `published: false` records `admin_declined_listing`.
10. Search and featured queries require `status: "active"`. Featured also requires `published: true`.

**Implemented behaviour that looks inconsistent:** when the owner sets status `inactive`, the Intercom event name is `host_requested_publish`. When the owner sets status `active`, the event name is `host_deactivated_listing`. **Requires confirmation** whether those event names are swapped.

`listing.requestPublish` only sets ActiveCampaign/Intercom attribute `LATEST_LISTING_PUBLISHED` to `"Yes"`. It does not itself change `status`.

### WF-4 — Request a booking and pay

1. `bookings.create` requires login. It prices the request on the server from the listing's pricing tier (or a thread `customRate`), writes a `pre_bookings` document, and does not yet write `bookings`.
2. Status on that document is `Pending Host Approval`. Reference id format is `FS{counter}-{D/M/YYYY}` via `CounterService`.
3. `bookings.preparePeachPaymentCheckout` starts a Peach checkout for `pricing.total`, currency `ZAR`, payment type pre-authorisation (`PA`), `createRegistration: true`, and any cards already stored on `profile.cards`.
4. The browser returns to `/booking/payment-result/:listingId/:preBookingId`. Success calls `bookings.getPeachPaymentsPaymentStatus`.
5. On Peach success code, the pre-booking is removed, a `bookings` row is inserted with the Peach payment id, the card is stored if new, the listing calendar is blocked, and the host has **3 days** (`approvalExpiresDate`). A 24-hour reminder job is scheduled. Failure sends the user back to the listing with `paymentError=1`.
6. Success sends the user to booking requests (`BookingRequests`).

Provenance: **Implemented.**

### WF-5 — Host accepts, declines, or lets the request expire

- **Accept** (`bookings.hostAccept`): Peach capture (`CP`) for `pricing.total`. On success, status becomes `Host Approved Booking`, calendar entry is marked approved, approval jobs are removed, a completion job is scheduled at `endDate`, and a 48-hour-before-start reminder email is scheduled only when the first timeslot starts more than 96 hours from now. PDFs are generated.
- **Decline** (`bookings.hostDecline`): Peach reversal (`RV`). Status becomes `Booking Cancelled`, cancellation type `Declined`. Timeout message is "The request timed out". Manual message is "The Host declined the booking request". Fees on the refund snapshot are zeroed.
- **Expiry:** job `bookings.onHostApprovalTimeExpires` calls `bookings.hostDecline(bookingId, true)`.

Capture or reversal failure throws and does not show a dedicated recovery workflow in the method.

Provenance: **Implemented.**

### WF-6 — Cancel, complete, review

- **Host cancel** after approval: full Peach refund (`RF`) of `pricing.total`, status `Booking Cancelled`, type `Cancelled`, fees zeroed, calendar entry removed, queued jobs for that booking removed.
- **Filmmaker cancel before approval:** Peach reversal, type `Declined`, fees zeroed.
- **Filmmaker cancel after approval:** if now is more than 48 hours before the first timeslot start, refund amount is 50% of `pricing.booking` plus `additionalFeesTotal`, and the host service fee is recalculated on half the booking amount. If inside that 48-hour window, `refund` stays 0, no Peach call is made, and `refundPricing` is left unset, but the booking is still marked cancelled.
- **Completion:** job `bookings.onBookingEndTime` sets status `Booking Completed Successfully` and pulls the calendar entry. `endDate` stored at payment time is the end of the last timeslot. `terminationDate` is that end plus 7 days. No method was found that reads `terminationDate` to change status.
- Reviews: `Reviews.createListingReview`, `Reviews.createUserReview`, `Reviews.createWebsiteReview`. Flags `hostHasReviewedBooking` and `filmmakerHasReviewedBooking`. Review emails are scheduled 48 hours out.

Provenance: **Implemented.** The 48-hour filmmaker-cancel branch with a zero refund is **inferred** from the control flow; there is no comment that states "no refund inside 48 hours" as a product rule. **Requires confirmation.**

### WF-7 — Message the host, including a custom rate

Dashboard routes under `/dashboard/messages`: inbox, thread, archive.

`Messaging.createThread` starts a thread from a listing and booking form fields. `Messaging.createCustomFee` stores `bookingFormFields.customRate` and `canOverrideMinHours`. `bookings.create` uses `customRate` when the thread has one, instead of the tier price.

`Messaging.createCustomFee` requires a logged-in user. The method body does not check that the caller is the host. **Requires confirmation** whether that is intentional.

### WF-8 — Dashboard

`/dashboard` requires login. Sub-routes:

- Listings: `/dashboard/my-listings/active|in-review|inactive|all`
- Favourites: `/dashboard/favourites` (Intercom `viewed_saved_listings`)
- Profile: `/dashboard/profile`
- Payments and payouts, password change, notification settings
- Bookings: requests, upcoming, past, cancelled, and a detail route for each
- Messages: inbox and archive

Opening a booking list marks that status's notifications seen and sends Intercom events. Every booking-list entry also sends `host_bookings_viewed`, including when the route is not host-specific. **Inferred** as a naming mismatch. **Requires confirmation.**

### WF-9 — Admin

`/admin` requires global `admin`, otherwise redirect `/`.

| Path | Page |
|---|---|
| `/admin/users`, `/admin/user/:userId` | Users |
| `/admin/listing-forms` | Which wizard forms apply to which categories |
| `/admin/listings`, `/admin/listing-detail/:listingId` | Listings, featured toggle, sort rating |
| `/admin/bookings`, `/admin/bookings/:bookingId` | Bookings, `booking.setPayoutMade` |
| `/admin/spaces-tags` | Space tags |
| `/admin/locations`, `/admin/locations/new`, `/admin/locations/:locationId` | Search polygons. New locations start `isPublic: false`, `isLanding: false` |

`admin.listing.sendChangeRequest` stores a change request. Admin-only checks are present on the admin methods that were read (`users.all.settings.set`, `admin.deleteAllWatermarkedImages`, location methods, `listings.setSortRating`, `bookings.getCount`, `booking.setPayoutMade`).

**Implemented defect candidate:** publication `AdminListings` uses an undefined variable `userId` in `Roles.userIsInRole(userId, 'admin', ...)`. The condition is `if (!this.userId && !Roles.userIsInRole(...))`. A logged-in non-admin therefore is not stopped by that condition. **Requires confirmation** and a code review. This PRD does not describe how to call it.

### WF-10 — Legal, contact, and stray routes

- `/legal?name=` tabs: `Terms-of-Use` (also renders Acceptable Use under the terms), `Privacy-Policy`, `Terms-of-Service`. Any other name selects Terms of Use.
- `/contact` contact form. Methods `user.sendContactUsEmail`, `user.sendGetInTouchEmail`, `user.sendFeedbackEmail`.
- `/loader` full-page loader. `/testing` renders the sexual-harassment policy with no login check. `/dev-test` renders the landing page. An admin check on `/dev-test` is commented out.
- `/booking/agreement/:bookingId` renders the agreement. `/booking/receipt/:bookingId/:actingAs/` and `/invoice/:bookingId/:actingAs/` render receipt and VAT invoice. `actingAs === "host"` selects the host variant. No login trigger is declared on those routes in the route file.

## Functional requirements

### FR-001 — Public home and search

The system shows a landing page at `/` and a search page at `/search` only when `center` is present. Search results are limited to listings with status `active`.

Evidence: `imports/routes/general/routes.jsx`, `imports/routes/listings/search.jsx`, `imports/api/Listings/both/utils/createListingQuery.js`, `listings.search`.

Implementation: Implemented. Confidence: High.

### FR-002 — Listing detail is public

`/filmspace-detail/:listingId` subscribes to `ListingDetail` and sets the title and Open Graph tags from the listing name, description, and photo. A login redirect on this route is commented out. Canonical host in meta tags is `https://everyspace.co.za`.

Implementation: Implemented. Confidence: High.

### FR-003 — Accounts

A person can register, verify email, sign in with password or Google or Facebook, reset a password, verify a mobile token, and be remembered in `localStorage`. Unverified password users and banned users cannot complete login.

Evidence: `imports/routes/users/auth.jsx`, `imports/api/Users/server/init/users.js`, `AccountSchema`.

Implementation: Implemented. Confidence: High.

### FR-004 — Acting mode

The client stores `userActingAs` as `filmmaker` or `host`. After login, if the user has listings the mode becomes `host`, otherwise `filmmaker`. The dashboard offers a switch to host when a listing exists.

Evidence: `imports/api/Users/client/init/users.js`, `DashboardMenuGuest.jsx`.

Implementation: Implemented. Confidence: High.

This mode is separate from the `host` role. **Requires confirmation** of which one gates host-only actions in the UI.

### FR-005 — Listing wizard and ownership

Only a logged-in user can create a listing. Only an `editor` of that listing or a global admin can open wizard steps. New listings are inactive and unpublished. Empty listings are deleted when leaving the address step.

Implementation: Implemented. Confidence: High.

### FR-006 — Listing categories

Seed data defines four parents: Residential, Commercial, Studio, Mobile, each with a fixed child list in `imports/api/Listings/server/init/categories.js`. Commercial includes a spelling `Commmercial Office` as written in source. `REWRITE` is false, so startup does not overwrite existing category documents unless that flag is changed.

Implementation: Implemented. Confidence: High.

### FR-007 — Admin publishes a listing

An admin moves a listing to `active` and `published`, or back to inactive and unpublished. A non-admin cannot change status while the listing is `in-review`.

Implementation: Implemented. Confidence: High.

### FR-008 — Pricing the filmmaker pays and the host is owed

See business rules BR-001 to BR-006. The server recalculates price inside `bookings.create` and stores the breakdown on the booking. Currency is ZAR.

Implementation: Implemented. Confidence: High for the formula in `PricingService.js`. Confidence: Medium that the always-on filmmaker fee waiver is still the commercial rule.

### FR-009 — Card payment lifecycle

The filmmaker's card is pre-authorised at request time, captured when the host accepts, reversed when the host declines, the request times out, or the filmmaker cancels before acceptance, and refunded when the host cancels after acceptance or when the filmmaker cancels early enough after acceptance. See WF-5 and WF-6.

Implementation: Implemented. Confidence: High.

### FR-010 — Host decision window

The host has 3 days. At 24 hours a reminder event is sent if status is still `Pending Host Approval`. At expiry the request is declined as a timeout.

Implementation: Implemented. Confidence: High.

### FR-011 — Calendar blocking

A successful payment adds the booking to the listing calendar. Decline, cancel, and completion remove it. `bookings.findOverlappingBookingRequests` returns other pending requests on the same listing whose timeslots overlap. The method returns them; this PRD did not find a server rule that rejects the second request automatically.

Implementation: Implemented for calendar updates and the overlap query. Automatic rejection of overlaps: **Unknown / Requires Confirmation.**

### FR-012 — Messages and custom fees

Filmmaker and host can exchange thread messages, archive a thread, and the host flow can attach a custom rate that replaces the tier price at booking creation.

Implementation: Implemented. Confidence: High.

### FR-013 — Payout details

The user can add, update, delete, and set a primary payout method. Schema requires account name, account type, bank name, digit-only account number, billing address, city, province, digit-only postal code, and an optional VAT number.

Evidence: `PayoutMethodSchema`, `user.addPayoutMethod` and related methods.

Implementation: Implemented. Confidence: High.

Whether money is paid to that bank account automatically is **Unknown**. `booking.setPayoutMade` is an admin flag. No payout-provider integration (no PayPal, EFT file, or Peach payout) was found.

### FR-014 — Documents

The system can generate a booking agreement PDF, a receipt, and a VAT invoice, with host and filmmaker variants selected by `actingAs`.

Evidence: `booking.generateBookingPDFs`, `booking.receipt.download`, `booking.invoice.download`, receipt routes.

Implementation: Implemented. Confidence: Medium on the exact legal wording (the documents are React/HTML templates; they were not fully transcribed).

### FR-015 — Reviews

After a booking, host and filmmaker review flags can be set. Listing, user, and website review methods exist. Notification emails are delayed 48 hours.

Implementation: Implemented. Confidence: Medium on who is allowed to submit (method-level checks were not fully transcribed).

### FR-016 — Admin operations

An admin can list and open users, listings, and bookings; edit category form mapping; manage space tags; manage location polygons and their public and landing flags; set a sort rating; mark a payout as made; send a listing change request.

Implementation: Implemented. Confidence: High.

### FR-017 — Contact and newsletter

Contact and feedback forms send email. Newsletter subscribe, unsubscribe, and is-subscribed methods exist against collection `newsletter`. Registration also inserts the new user's email into that collection.

Implementation: Implemented. Confidence: High.

### FR-018 — Analytics and chat

Each route change (except the first run) sends a GTM page view, a Facebook page view, and an Intercom update. Search route does not scroll to top, because mobile filter changes were changing the route.

Implementation: Implemented. Confidence: High.

### FR-019 — Notifications preference

User settings include `send_chat_messages_to_cellphone`, default true. The dashboard has a notification settings page that calls `user.settings.set`.

Implementation: Implemented for the setting. Confidence: Medium that every chat message honours it. SMS sending exists as `sendSMS`.

## Important constraints

- Meteor 2.4 and Node 14.18.1 for the Docker production image.
- Payments are ZAR through one Peach entity. Sandbox constants in `PeachPaymentsService.js` are commented out. The live constants are what the running code uses.
- Filmmaker service-fee VAT rate in code is 15% (`amount * 0.15`). Same for the host fee.
- Search is map-centred. No centre means no search page.
- Listing wizard content depends on Mongo `forms_for_categories`, not only on code.
- Job execution is in-process `Meteor.setTimeout`. On startup, jobs dated before now are deleted and remaining jobs are reset to pending. A daily cron at 08:00 (server local time, no timezone argument) re-enqueues. Failed jobs are logged and left in place. The comment in code says retry behaviour should be defined; the failure branch only logs.
- Multiple server processes would each reset and schedule jobs on startup. **Requires confirmation** of how many Node processes run in production.
- `insecure` and `autopublish` Meteor packages are commented out.

## Planned or unfinished (visible in code)

| Item | Evidence | Status |
|---|---|---|
| Server-side render | Import commented out in `server/main.js` | **Planned** or abandoned. Not running |
| `/matthew-test` and `/marketing` routes | Commented out | Not routed |
| Admin gate on `/dev-test` | Commented out | Not enforced |
| Login wall on listing detail | Commented out | Not enforced |
| Peach sandbox endpoint | Commented out | Not used |
| DigitalOcean Spaces endpoint | Commented out | Not used by the client constructor |
| Job retry | Comment only | Not implemented |
| `bookings.setExpiresIn` and `bookings.setEndDateIn` | Methods exist to move job times | **Implemented** helpers. No UI requirement found. Likely operational |

## Requirements traceability sample

```text
FR-009 — Card payment lifecycle

Requirement:
A booking request pre-authorises the filmmaker card, captures on host accept,
reverses on decline, timeout, or pre-approval cancel, and refunds on host cancel.

Evidence:
- imports/api/Bookings/server/methods/bookings.js
- imports/api/PeachPayments/server/services/PeachPaymentsService.js
- imports/routes/bookings/bookings.jsx

Implementation:
Implemented

Confidence:
High
```
