# PRD — filmspace (Everyspace)

Evidence date: 2026-10-15.

Source: `git@bitbucket.org:overdrive/filmspace.git`, `main` at `f19c1dc` (2024-11-14, "first commit").

There is no prior PRD in the source repository or in Silicon Ark. Requirements below are recovered from the implementation and from `README.md`. Provenance is labelled. Committed secrets are not reproduced.

## Product purpose

**Implemented.** A logged-in guest can search for a space, save it, message the host, and submit a paid booking request in South African rand. A host can create a listing, submit it for review, and accept or decline booking requests. An admin can approve listings, manage users, locations, and space tags, and mark a payout as made.

**Documented** on the landing page (`HowEverySpaceWorks.tsx`): guests discover spaces, book, then meet, create, or celebrate. Hosts list without an up-front charge, accept the bookings they want, and receive a payout. The same component says hosts get "R1 000 000 liability insurance". No insurance workflow exists in code. **Unknown / Requires Confirmation.**

**Documented** in the README contact and legal meta is not the only copy. The contact route meta description says "Contact Everyspace for help listing or renting unique film-friendly property and locations." Activity enums also include meetings, parties, weddings, fitness, and other non-film uses (`imports/api/Activities/both/enums.ts`). The product is a general space marketplace with film and photo as one activity family, not a film-only tool.

## Users and roles

| Actor | Evidence | Status |
|---|---|---|
| Guest (filmmaker in code and emails) | Booking field `filmmakerId`, dashboard bookings and favourites | **Implemented** |
| Host | Global role `host`, listing `createdBy`, payout methods | **Implemented**. `toggleUserIsHost` adds or removes the role. The method does not check that the caller is an admin |
| Admin | Global role `admin`. `/admin` redirects everyone else to `/` | **Implemented** |
| Listing editor | Role `editor` scoped to the listing id, granted in `listing.create` | **Implemented** |
| Bootstrap admins | `create_admin.js` ensures three email addresses exist and have `admin` | **Implemented**. Password when the user is created: `admin` |
| Banned user | `Accounts.validateLoginAttempt` rejects `user.banned` | **Implemented** |
| Deploy operator | GitLab CI, Galaxy `deploy.bat`, README SSH commands | **Inferred** from those files. No in-app deploy screen |

Password login also requires the profile email to be verified (`users.js` `validateLoginAttempt`). Google and Facebook sign-in skip that password-type check.

## Systems the product talks to

1. The visitor's browser (Meteor client, Google Tag Manager id `UA-158596565-1`, Facebook `fbq` page view, Intercom `update` on route change). `imports/routes/router.js`.
2. MongoDB. Connection comes from `MONGO_URL` or Meteor's local database. Several committed URLs exist; see technical documentation. Values are not copied here.
3. Peach Payments OPPWA at `https://oppwa.com` (live constants in `PeachPaymentsService.js`). Sandbox `test.oppwa.com` constants are commented out.
4. SMTP, via Meteor `Email` and `MAIL_URL`.
5. Twilio SMS from `imports/api/Users/server/init/sms.js`. In development, `SMSNotifications` prints the body and does not call Twilio.
6. ActiveCampaign (`activecampaign` package, account URL host `filmspace.api-us1.com` in settings).
7. Intercom (`intercom-client`, `EveryspaceIntercomClient`).
8. Object storage through the AWS SDK. Bucket `filmspace-assets`, region `af-south-1`. A DigitalOcean endpoint is commented out.
9. Google Maps, using `public.GOOGLE_API_KEY` from settings. reCAPTCHA siteverify in `recaptchaVerify`.
10. Google and Facebook OAuth, configured on startup in `authentication_services.js`.

## Major workflows

### WF-1 — Open the marketing home

`/` mounts `EveryspaceLandingPage` inside the dev layout (`imports/routes/general/routes.jsx`). Sections: hero, "how everyspace works" (guest and host), activity search prompts, location slider, and a link to `ListingCreate`.

`/legal` shows `LegalPage` with a query param `name` selecting the tab. `/contact` shows `ContactUsPage`. Title and meta description are set on those two routes.

`/loader`, `/testing`, and `/dev-test` are also registered. `/testing` renders the sexual-harassment policy component. `/dev-test` renders the landing page again. The admin gate on `/dev-test` is commented out.

Provenance: **Implemented**.

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

Routes in `imports/routes/users/auth.jsx`:

| Path | Name |
|---|---|
| `/create-account` | create account |
| `/register` | register |
| `/login` | login |
| `/login/someone-else` | login as someone else |
| `/request-reset-password` | request reset |
| `/reset-password/:token/:userId` | set a new password |
| `/verify-email/:token` | verify email |
| `/verify-mobile/:token` | verify mobile |
| `/verify-your-email` | prompt to verify |

Account fields (`AccountSchema`): first name at least 3 characters, last name, email, password, confirm password, mobile number at least 10 characters and matching the phone regex, country code default `27`, optional company, website, about, VAT number, city, filmmaker types. Newsletter subscription defaults to true. Confirm password must match.

Password login fails when the email is unverified or the user is banned.

Provenance: **Implemented**.

### WF-3 — List a space

`/listing/*` requires a logged-in user. If the route has a `listingId`, the user must have the `editor` role on that listing or be a global admin. Otherwise the router sends them home.

`/listing/create` calls `listing.create`, which inserts a listing with `status: "inactive"`, `published: false`, and grants `editor` on that id. The user is then sent to the address step.

The wizard routes cover address, category, category-specific detail (residential, commercial, studio, mobile), parking, amenities, restrictions, photos, many style forms, description, name, rules, calendar, activities, pricing, and additional fees. Which style forms apply is driven by `forms_for_categories` for the listing's subcategories.

Listing statuses used in code: `inactive`, `in-review`, `active`. Search and the featured publication require `status: "active"` and `published: true`. While status is `in-review`, only an admin may change it (`listing.status.set`).

A committed BSON dump `db/meteor/categories.bson` contains category names including Residential, Commercial, Studio, Mobile, and many subtypes (House, Film Studio, Rooftop, and others). The dump is a snapshot in git, not a seed that the app runs on startup. **Unknown / Requires Confirmation** whether production still uses that tree. Stored spellings in the dump include `Commmercial Office`, `Barbeshop / Salon / Spa`, and `Airport / Airplane Hanger`.

Provenance: **Implemented**, except the live category tree, which is **Unknown** beyond the dump.

### WF-4 — Search and open a listing

`/search/:page?` is the search page. `listings.search` returns listings only when `location.center` is non-empty and `activities` is non-empty. Otherwise it returns empty arrays. The geo query uses a 2dsphere index on `address`, `maxDistance` 45 000 (45 km), and only active published listings. Further filters: subcategory types, attendee range or exact attendees, price min/max, styles, amenities, and a space tag. Sort weight uses distance bands of 15 km and the admin `sort_rating`.

`/filmspace-detail/:listingId` is the public detail route.

Attendee ranges in the search method: `1 to 25`, `26 to 50`, `51 to 100`, `100plus`.

Provenance: **Implemented**.

### WF-5 — Message the host and set a custom rate

`Messaging.createThread` starts a thread from a listing. The messaging README describes a "Contact Host" modal with timeslots, crew size, shoot type, and a message. That README is a design note and still says the click handler lives in `Asdasd.jsx`, which is not a file in this tree.

`Messaging.createCustomFee` stores `bookingFormFields.customRate` and `canOverrideMinHours` on the thread and pushes a `CUSTOM_FEE_GIVEN` message. `bookings.create` uses that custom rate when a `threadId` is passed and the thread has `customRate`.

Threads can be archived and restored per side (`hostHasArchived`, `filmmakerHasArchived`).

Provenance: custom rate and threads are **Implemented**. The modal layout in the messaging README is **Documented** and not verified against a file named `Asdasd.jsx`.

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

`bookings.create` requires a logged-in user. It prices the request on the server, validates with `BookingsSchema`, and inserts a `pre_bookings` document. It does not yet charge the card.

`bookings.preparePeachPaymentCheckout` asks Peach for a checkout id. Payment type is pre-authorisation (`PA`). Currency is `ZAR`. Amount is `pricing.total`. Saved card registration ids are attached when the user has `profile.cards`.

`bookings.getPeachPaymentsPaymentStatus` loads the Peach result. On the success code it:

- removes the pre-booking,
- stores a new card registration when Peach returns one,
- inserts a `bookings` document with status `Pending Host Approval`,
- sets `approvalExpiresDate` to three days ahead and `terminationDate` to seven days after the last timeslot end,
- blocks the listing calendar,
- schedules host-timeout and a 24-hour reminder.

Any other Peach code throws `failed`.

Reference format: `FS` + counter + `-` + day/month/year.

Provenance: **Implemented**.

### WF-7 — Host accepts, declines, or the request times out

`bookings.hostAccept` captures the pre-authorisation (`CP`) for `pricing.total`. On success the status becomes `Host Approved Booking`, the timeout jobs are removed, and `bookings.onBookingEndTime` is scheduled at `endDate`. A 48-hour reminder is scheduled only when the first timeslot starts more than 96 hours from now. The reminder time is 48 hours before that start. Capture failure throws `Payment capture failed`.

`bookings.hostDecline` reverses the pre-authorisation (`RV`). On success the status becomes `Booking Cancelled`, cancellation type `Declined`. The timeout path passes `didTimeout = true` and stores the message "The request timed out". The host-initiated path stores "The Host declined the booking request".

`bookings.onHostApprovalTimeExpires` calls `bookings.hostDecline` with the timeout flag.

Provenance: **Implemented**. Neither accept nor decline checks that `this.userId` is the host. **Requires Confirmation** whether the UI is the only guard.

### WF-8 — Cancel after a decision

`bookings.hostCancel` refunds `pricing.total` (`RF`) and marks the booking cancelled, type `Cancelled`.

`bookings.filmmakerCancel` while status is still pending reverses the pre-authorisation.

`bookings.filmmakerCancel` after approval: if now is more than 48 hours before the first timeslot start, the refund amount is 50% of `pricing.booking` plus `additionalFeesTotal`, and that amount is refunded. The host payout on the refund record is that half booking amount minus the host service fee (15% plus VAT on the fee). If now is inside that 48-hour window, `refund` stays 0, Peach is not called, and the booking is still marked cancelled. `refundPricing` can be unset in that branch.

Provenance: **Implemented**.

### WF-9 — Complete, review, and documents

`bookings.onBookingEndTime` sets status `Booking Completed Successfully` and pulls the booking off the listing calendar.

Review methods: `Reviews.createListingReview`, `Reviews.createUserReview`, `Reviews.createWebsiteReview`. Flags `hostHasReviewedBooking` and `filmmakerHasReviewedBooking` are set by separate methods. Review emails are queued through the job queue. From address is `Everyspace <hello@everyspace.co.za>`.

`booking.generateBookingPDFs` deletes existing documents for the booking and regenerates host and filmmaker receipts and invoices, plus the agreement PDF, using Puppeteer against the in-app receipt and invoice routes. Files are written locally and uploaded to `filmspace-assets`.

Receipt id format: `REC` + counter + `-` + day/month/year.

Routes: `/booking/receipt/:bookingId/:actingAs/`, `/invoice/:bookingId/:actingAs/`, `/booking/agreement/:bookingId`, `/booking/payment-result/:listingId/:preBookingId`.

Provenance: **Implemented**.

### WF-10 — Admin

`/admin` requires global role `admin`.

| Path | Screen |
|---|---|
| `/admin/users` | users |
| `/admin/user/:userId` | one user |
| `/admin/listing-forms` | which forms belong to which categories |
| `/admin/listings` | listings |
| `/admin/listing-detail/:listingId` | one listing |
| `/admin/bookings` | bookings |
| `/admin/bookings/:bookingId` | one booking |
| `/admin/spaces-tags` | space tags |
| `/admin/locations` | locations |
| `/admin/locations/new` and `/admin/locations/:locationId` | location editor |

`admin.listing.sendChangeRequest` stores a change request. `listings.setSortRating` is admin-only. `booking.setPayoutMade` is admin-only. `users.all.settings.set` can flip `banned`.

Provenance: **Implemented**.

## Functional requirements

### FR-001 — Account registration

Requirement: A visitor can register with name, email, password, and a South African-style mobile number. Passwords must match. First name is at least 3 characters. Mobile is at least 10 characters.

Who: visitor. Trigger: submit on `/create-account` or `/register`. Inputs: `AccountSchema`. Output: a Meteor user. Validation: SimpleSchema messages for mismatch, email in use, and failed reCAPTCHA ("You don't seem human"). Error: unverified users cannot password-login.

Evidence: `imports/api/Users/both/schemas/users.js`, `imports/routes/users/auth.jsx`.

Implementation: **Implemented**. Confidence: High.

`public.DISABLE_RECAPTCHA` is `1` in `dev.settings.json`. Whether production enforces reCAPTCHA is **Unknown / Requires Confirmation**.

### FR-002 — Social sign-in

Requirement: A visitor can sign in with Google or Facebook. The app upserts both service configurations on startup.

Who: visitor. Trigger: OAuth popup. Output: a user, and for a new social user an email is added and newsletter handling runs (`afterUserCreated`).

Evidence: `.meteor/packages` (`accounts-google`, `accounts-facebook`), `authentication_services.js`.

Implementation: **Implemented**. Confidence: High. The stored app secrets are committed. Do not copy them into new environments.

### FR-003 — Email and mobile verification

Requirement: Password login is refused until the profile email is verified. The user can request a verification email and a mobile verification.

Who: account holder. Trigger: login attempt, or verify actions. Error codes: `validateLoginAttempt.userEmailNotVerified`, `validateLoginAttempt.userBanned`.

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

Implementation: **Implemented**. Confidence: High.

### FR-004 — Roles

Requirement: Admin routes and admin methods require global role `admin`. Listing edit routes require `editor` on that listing or global `admin`. Host is a separate global role.

Who: the router and methods. Trigger: navigation or method call.

Evidence: `imports/routes/admin/routes.jsx`, `imports/routes/listings/forms.jsx`, `toggleUserIsHost`.

Implementation: **Implemented**. Confidence: High for the route gates. Confidence: Medium for method-level checks, because several booking methods do not test `this.userId` against `hostId` or `filmmakerId`.

### FR-005 — Create and publish a listing

Requirement: A logged-in user can create an inactive unpublished listing and walk the wizard. Only an admin can move a listing out of `in-review`. Public search shows a listing only when it is `active` and `published`.

Who: host (any logged-in user can call `listing.create`). Trigger: `/listing/create`, then step saves, then `listing.status.set`. Inputs: step schemas in `imports/api/Listings/both/schemas`. Output: `listings` document.

Evidence: `imports/api/Listings/both/methods/listings.js`, `imports/api/Listings/server/methods/search.ts`.

Implementation: **Implemented**. Confidence: High.

### FR-006 — Search

Requirement: Search returns a page of cards and map pins for active published listings within 45 km of the chosen centre, matching the selected activity and any extra filters. If centre or activities are missing, the method returns no listings.

Who: visitor. Trigger: `/search`. Inputs: `SearchListingsParams`. Output: `{ listings, mapResults, count }`.

Evidence: `listings.search` in `search.ts`.

Implementation: **Implemented**. Confidence: High.

### FR-007 — Favourites

Requirement: A logged-in user can toggle a listing as a favourite and store a note on it.

Who: logged-in user. Methods: `user.toggleListingFavourited`, `user.favourite.note.update`. Route: `/dashboard/favourites`.

Implementation: **Implemented**. Confidence: High.

### FR-008 — Messages stay on the platform

Requirement: Users exchange messages on a thread. A new message schedules an SMS and email after 5 minutes if that message is still unseen. A follow-up unread notice is scheduled 25 minutes after that job runs. A separate daily job at 15:00 summarises unread counts, and that job alone respects `settings.send_chat_messages_to_cellphone`.

Who: host or guest on the thread. Trigger: `Messaging.createMessageInThread`. Output: thread message, then SMS and email.

Evidence: `message_threads.js`, `imports/api/Messaging/server/jobs/notifications.js`.

Implementation: **Implemented**. The README story US-7 ("Keep all comms on Filmspace") is consistent with in-app threads. The 5-minute path does not read the SMS preference. Confidence: High for the timers. The preference gap is a discrepancy.

### FR-009 — Pricing a booking

Requirement: The server prices a booking as rate × hours, plus additional fees, minus a percentage discount that applies only when hours are greater than 8. The filmmaker service fee is 5% of the booking fee, plus 15% VAT on that fee. The host service fee is 15% of the booking fee, plus 15% VAT on that fee. The amount stored as `pricing.total` subtracts the filmmaker fee (labelled `promotionDiscount` with the comment `PROMOTION_FEATURE`).

Who: server, inside `bookings.create`. Inputs: timeslots, crew tier or exact attendees, listing additional fees, optional thread custom rate. Output: `pricing` on the pre-booking. Currency: ZAR.

Evidence: `PricingService.js`, `bookings.create`.

Implementation: **Implemented**. The README names US-8 "Zero Service Fees". The code zeroes the filmmaker fee on the charged total and still deducts the host fee from payout. Confidence: High for the formula. Whether the waiver is still the business rule: **Requires Confirmation**.

### FR-010 — Pay by card pre-authorisation

Requirement: The guest completes a Peach pre-authorisation for `pricing.total` before the request exists as a booking. Success creates the booking and blocks the calendar. Failure does not create the booking.

Who: guest. Trigger: checkout then return to `/booking/payment-result/:listingId/:preBookingId`. Error: Meteor error `failed`.

Evidence: `bookings.preparePeachPaymentCheckout`, `bookings.getPeachPaymentsPaymentStatus`.

Implementation: **Implemented**. Confidence: High. Live Peach credentials are hard-coded.

### FR-011 — Host decision window

Requirement: The host has three days to accept. A reminder event is scheduled at 24 hours if the booking is still pending. If the window ends, the pre-authorisation is reversed and the booking is declined as timed out.

Who: host, or the job queue. Trigger: accept, decline, or `bookings.onHostApprovalTimeExpires`.

Evidence: `bookings.js` methods and `imports/api/Bookings/server/jobs/bookings.js`.

Implementation: **Implemented**. Confidence: High.

### FR-012 — Cancellation money

Requirement: Host cancel refunds the full charged total. Guest cancel before approval reverses the pre-authorisation. Guest cancel after approval, more than 48 hours before the start, refunds half the booking fee plus additional fees. Inside 48 hours the code records a cancellation and does not call Peach.

Who: host or guest. Inputs: `cancelForm.reason` and `cancelForm.message` for the cancel methods.

Implementation: **Implemented**. Confidence: High for the branches. Whether a zero refund inside 48 hours is the approved policy: **Requires Confirmation**, because it is not written down outside the code.

### FR-013 — Payout record

Requirement: Host payout on a new booking is booking fee − discount + additional fees − host service fee including VAT. Admins can set `booking.setPayoutMade`. Hosts store bank payout methods (account name, bank, digit-only account number, billing address).

Who: server calculates; admin marks paid; host edits methods. Methods: `user.addPayoutMethod`, `user.updatePayoutMethod`, `user.deletePayoutMethod`, `user.setPrimaryPayoutMethod`.

Implementation: **Implemented** as stored data and a flag. No bank transfer integration is in the code. Confidence: High.

### FR-014 — Reviews

Requirement: After a booking, a listing review and a user review can be stored, and each side can be marked as having reviewed. Emails tell the filmmaker and the host.

Implementation: **Implemented**. Confidence: Medium on exactly when the UI allows the review form. The methods themselves do not show a "booking must be completed" check in the portions read for this PRD.

### FR-015 — Documents

Requirement: The system can generate a host receipt, a filmmaker receipt, a host invoice, a filmmaker invoice, and a booking agreement PDF, then store them.

Who: server after accept, decline, or cancel (`booking.generateBookingPDFs`). Output: `DocumentsDB` file plus object in `filmspace-assets`.

Implementation: **Implemented**. Confidence: High. Generation depends on Puppeteer and Chromium (installed in the Docker image).

### FR-016 — Admin moderation

Requirement: An admin can list and open users, listings, and bookings; edit locations and whether they are public or on the landing page; edit space tags and which activities and images they apply to; send a listing change request; feature a listing; set sort rating; delete watermarked images in bulk.

Implementation: **Implemented**. Confidence: High.

### FR-017 — Contact and feedback

Requirement: A visitor can send a contact message (`user.sendContactUsEmail`) and a get-in-touch message (`user.sendGetInTouchEmail`). A logged-in user can send feedback (`user.sendFeedbackEmail`).

Implementation: **Implemented** as email methods. Confidence: Medium on the exact recipient address without quoting mailbox secrets from settings.

### FR-018 — Notifications preference

Requirement: The account settings page can set `send_chat_messages_to_cellphone` (default true).

Implementation: **Implemented** for storage and for the 15:00 summary. **Not applied** to the per-thread SMS jobs. See FR-008.

## Integrations

| System | Purpose | Direction | Auth as implemented | Failure behaviour |
|---|---|---|---|---|
| Peach Payments | Card hold, capture, reversal, refund | Outbound HTTPS | Bearer token constant in source | Method throws `failed`, `Payment capture failed`, `Reversal failed`, or `Refund failed` |
| MongoDB | System of record | App reads and writes | URL in env or Meteor default | Not specially handled beyond Mongo driver errors |
| SMTP | Transactional email | Outbound | `MAIL_URL` | Meteor email errors. Not uniformly caught |
| Twilio | SMS | Outbound | Sid and token constants | `sendSMS` logs and swallows the error |
| ActiveCampaign | Contact fields and events | Outbound | URL and key in settings or env | Not fully traced in this pass |
| Intercom | In-app chat and events | Outbound | Client wrapper | Not fully traced |
| Object storage | Images and PDFs | Outbound S3 API | `DO_ACCESS_KEY` / `DO_SECRET` | Upload callback logs the error |
| Google Maps | Listing address and search map | Browser | Public API key in settings | **Unknown** UI fallback |
| reCAPTCHA | Signup | Outbound siteverify | Secret constant in `recaptchaVerify` | Schema error `recaptchaFailed` when the app checks it |
| Google / Facebook | Login | OAuth | Secrets upserted on startup | Meteor accounts errors |
| GTM and Facebook pixel | Page analytics | Browser | Measurement id in `router.js` | No server check |

## Important constraints

- Currency in Peach calls is `ZAR`.
- VAT rate used on service fees is 15% (`ServiceFee.getVAT`).
- Search radius is 45 km.
- Host decision window is 3 days from successful pre-authorisation.
- Guest refund window after approval is 48 hours before the first timeslot.
- Listing images are watermarked (`listings.ensureWatermarkedImages`, sharp).
- Dashboard routes require a logged-in user and otherwise redirect to `/login`.
- Job-queue startup marks every job pending and deletes jobs dated before now (`job-queue.js`).

## Non-goals visible in the tree

- The Foundation gulp site under `src/` is not the Meteor UI.
- Postgres in the Helm chart is disabled and unused by the app.
- Server-side render startup is commented out.
- There is no in-app bank payout transfer. Payout is a recorded flag.
- Insurance is a sentence on the landing page only.

## Open questions

- Is Everyspace or Filmspace the name to use in new copy?
- Is the filmmaker 5% fee still waived on every booking?
- Is a guest cancellation inside 48 hours of the start meant to forfeit the whole charge?
- Which deploy target is production?
- Are the category names in `db/meteor/categories.bson` still production data?
- Should booking methods reject callers who are not the host or the guest on that booking?
