# Business rules — everspace (Everyspace)

Evidence date: 2026-10-15.

Source: `master` at `c784099`. Rules are taken from code. Each rule is marked **Documented** only when copy or README states it. Otherwise it is **Inferred** or **Implemented** from code. No commercial policy document is in the repository. Do not treat an inference as an approved policy until someone confirms it.

Secret values are not copied into this file.

## Identity and access

### BR-A1 — Password login requires a verified email

**Implemented.** `Accounts.validateLoginAttempt` rejects `type === "password"` unless an email on the user equals `profile.email` and `verified` is true. Error text: "Login Forbidden: User must Verify their email address".

Google and Facebook attempts are not rejected by that clause.

### BR-A2 — Banned users cannot log in

**Implemented.** If `user.banned` is true, login throws "Login Forbidden: User is Banned". New users are stored with `banned: false`.

### BR-A3 — Registration fields

**Implemented** in `AccountSchema`:

- First name at least 3 characters.
- Email must include a TLD (`SimpleSchema.RegEx.EmailWithTLD`).
- Password and confirm password must match. Same for new password and confirm new password.
- Mobile number at least 10 characters and must match `SimpleSchema.RegEx.Phone`.
- Default country calling code `27`.
- Newsletter subscription defaults to true.
- Passwords that do not match: "The passwords do not match".
- reCAPTCHA failure message: "You don't seem human".

### BR-A4 — Who may edit a listing

**Implemented.** Wizard routes allow the user only when they are logged in and either have role `editor` on that listing id or role `admin` globally. `listing.create` grants `editor` to the creator. `listing.update` and `listing.status.set` allow the creator or a global admin. While status is `in-review`, only an admin may change status.

### BR-A5 — Admin area

**Implemented.** `/admin` redirects to `/` unless the user has role `admin` in `Roles.GLOBAL_GROUP`.

### BR-A6 — Bootstrap admins

**Implemented.** Startup ensures three addresses exist and have role `admin`: `dean@codeswop.com`, `regardt@codeswop.com`, `hello@everyspace.co.za`. Missing users are created with a hardcoded password and the email marked verified.

**Requires confirmation.** This is a deployed behaviour, not a written policy. The password is in source and should be rotated.

### BR-A7 — Host role toggle has no caller check in the method

**Implemented.** `toggleUserIsHost(_id)` adds or removes global role `host` for the given id. The method body does not test `this.userId` or admin role.

**Requires confirmation** whether this is an oversight.

### BR-A8 — Acting as host is a browser flag

**Implemented.** `userActingAs` in `localStorage` and `Session` is `filmmaker` or `host`. It is not checked in the server methods read for this document. Server rules use `createdBy`, `hostId`, `filmmakerId`, and roles.

### BR-A9 — Dashboard requires login

**Implemented.** `/dashboard` redirects to `/login` when there is no user id. If the route group name is `HostListings`, the attempted path is stored in `Session` key `redirectAfterLogin`.

## Listings

### BR-L1 — A new listing is not public

**Implemented.** `listing.create` sets `status: "inactive"` and `published: false`.

### BR-L2 — Search only returns active listings

**Implemented.** `createListingQuery` and `listings.search` use `status: "active"`. Featured listings also require `published: true` and `isFeatured: true`, limit 6.

### BR-L3 — Status values

**Implemented.** Comment on `listing.status.set`: `in-review`, `active`, `inactive`. Host dashboard filters use those three strings.

### BR-L4 — Categories

**Implemented** seed list (parents): Residential, Commercial, Studio, Mobile. Children are the arrays in `imports/api/Listings/server/init/categories.js`. The flag `REWRITE` is false.

### BR-L5 — Pricing tiers drive the rate

**Implemented.** `bookings.create` selects a tier where `exactAttendees` is between `tier.min` and `tier.max` when `attendees` is missing and `exactAttendees` is present. The rate is `attendees.price` unless the message thread has `bookingFormFields.customRate`.

### BR-L6 — Empty listing cleanup

**Implemented.** Exiting the address route calls `listing.deleteIfEmpty`. The server method allows delete when the caller is admin or `createdBy`.

### BR-L7 — Sort rating is a non-negative integer

**Implemented.** `listings.setSortRating` is admin-only. The value is `Math.abs(parseInt(...))`. Non-finite becomes 0.

### BR-L8 — New map locations are hidden

**Implemented.** `locations.create` forces `isLanding: false` and `isPublic: false`. Only an admin may create, update, delete, or toggle those flags. The full location document including polygon is admin-only via `locations.get`. Public UI queries `isPublic: true` (landing also `isLanding: true`).

## Pricing and fees

All figures below are **Implemented** in `PricingService.js` and `bookings.create`. VAT rate is the literal `0.15`.

### BR-P1 — Booking fee

Booking fee = rounded(rate × hours). Hours are the sum of timeslot durations. Rounding is half-up to cents: `Math.round(n * 100 + Number.EPSILON) / 100`.

### BR-P2 — Duration discount

`setDiscount` applies `custom_discount / 100` only when total hours are greater than 8. Otherwise the discount is 0. The discount amount is that fraction of the booking fee (not of the total including fees).

### BR-P3 — Additional fees

Each listing `additional_fees` price that is a finite number is added. They are not multiplied by hours in `PricingService`.

### BR-P4 — Service fees

| Party | Percent of booking fee | VAT on that fee |
|---|---|---|
| Filmmaker | 5% (`0.05`) | 15% of the fee amount |
| Host | 15% (`0.15`) | 15% of the fee amount |

Filmmaker fee including VAT = fee × 1.15. Same pattern for the host.

### BR-P5 — Early-bird filmmaker fee waiver

**Implemented.** `bookings.create` sets `promotionDiscount` to the full VAT-inclusive filmmaker service fee, and sets `pricing.total` to `getTotal() - that fee`. The filmmaker total therefore excludes the filmmaker service fee.

UI tooltip (`BookingDetailPage.jsx`): "Guess what? Early bird filmmakers like you don't have to pay a service fee." It links to an Intercom article. There is no date, user-cohort, or listing condition in `bookings.create`. The waiver runs for every booking that method creates.

**Requires confirmation** that an always-on waiver is still the business rule. README lists "US-8 Zero Service Fees" as a March 2021 story.

The host service fee is not waived by this promotion.

### BR-P6 — What the filmmaker is asked to pay

`getTotal()` = booking fee + additional fees − discount amount + VAT-inclusive filmmaker service fee.

Amount sent to Peach is `pricing.total` after the promotion subtraction, formatted to 2 decimal places.

### BR-P7 — What the host payout figure is

`getPayout()` = booking fee − discount amount + additional fees − VAT-inclusive host service fee.

This number is stored as `pricing.payout`. No code found transfers that amount to a bank. An admin can set a payout-made flag (`booking.setPayoutMade`). **Requires confirmation** of the real payout process.

### BR-P8 — Currency

**Implemented.** Peach calls that set a currency use `ZAR`.

### BR-P9 — Custom rate replaces the tier

**Implemented.** If the thread has `customRate`, that number is the rate. `Messaging.createCustomFee` records the rate and `canOverrideMinHours`. The custom-fee method requires login and does not check the host role.

### BR-P10 — Minimum hours on a timeslot

**Implemented** in `TimeslotSchema`: minimum hours come from `chosenCrewSize.hours`, or 1 if that field is not set. The custom error key is the string `"Error"` when the end hour is too close to the start hour. The comparison uses `end.hour()` and `start.hour()` (clock hours, not a full duration). **Requires confirmation** that this matches the intended minimum-hours rule, because a booking that crosses midnight would not be measured in total elapsed hours by this check.

## Bookings

Status strings (**Implemented** in `BookingStatusEnums`):

| Constant | Stored value |
|---|---|
| Pending | `Pending Host Approval` |
| Approved | `Host Approved Booking` |
| Cancelled | `Booking Cancelled` |
| Completed | `Booking Completed Successfully` |

Cancellation type: `Declined` or `Cancelled`.

### BR-B1 — A request is not a booking until payment pre-authorisation succeeds

**Implemented.** `bookings.create` inserts `pre_bookings` only. `bookings.getPeachPaymentsPaymentStatus` moves it into `bookings` when Peach returns the success code, then deletes the pre-booking.

### BR-B2 — Reference numbers

**Implemented.** `FS` + next counter + `-` + day/month/year (`D/M/YYYY`).

### BR-B3 — Host has 3 days

**Implemented.** `approvalExpiresDate` is now plus 3 days. Job `bookings.onHostApprovalTimeExpires` declines the booking with `didTimeout = true`. A separate job 24 hours after payment sends Intercom `host_booking_request_reminder` only if status is still pending.

### BR-B4 — Accept captures the card

**Implemented.** Host accept sends Peach payment type `CP` (capture) for `pricing.total`. Only the Peach success code updates the booking to approved. Failure throws "Payment capture failed".

### BR-B5 — Decline and timeout reverse the pre-authorisation

**Implemented.** Decline and timeout send Peach type `RV` (reversal). Stored refund snapshot: amount = previous total, payout 0, both service fees zero. Reasons:

- Timeout: "The request timed out"
- Host: "The Host declined the booking request"
- Cancellation reason field on timeout and decline: "Booking Declined"

### BR-B6 — Host cancel after approval refunds the full total

**Implemented.** Peach type `RF` for `pricing.total`. Refund snapshot zeroes fees and payout. Failure throws "Refund failed" and does not reach the status update (the update is after the success check).

### BR-B7 — Filmmaker cancel before approval reverses

**Implemented.** Same reversal as decline. Cancellation type stored as `Declined` even though the filmmaker cancelled. **Requires confirmation** that "Declined" is the right label for a filmmaker withdrawal.

### BR-B8 — Filmmaker cancel after approval

**Implemented** in `bookings.filmmakerCancel`:

- Let `startMin48hours` be the first timeslot start minus 48 hours.
- If now is before that moment, refund = round(50% of `pricing.booking` + `additionalFeesTotal`). That amount is sent as a Peach refund. Host service fee on the refund snapshot is 15% plus VAT, calculated on 50% of `pricing.booking` only (additional fees are in the refund amount but not in that fee base).
- If now is not before that moment, refund stays 0, Peach is not called, and `refundPricing` is not set. The booking is still updated to cancelled.

**Requires confirmation.** The "no refund inside 48 hours" outcome is what the code does. It is not written as a sentence in a policy file. Terms of Service in the UI were not line-checked against this formula.

### BR-B9 — Completion

**Implemented.** At `endDate` (end of the last timeslot, stored when payment succeeds), status becomes `Booking Completed Successfully` and the calendar entry with that `bookingId` is pulled off the listing.

`terminationDate` is last timeslot end plus 7 days. No reader of that field was found in the booking methods. **Unknown** what the 7 days is for.

### BR-B10 — Reminder before the shoot

**Implemented.** If the first timeslot starts more than 96 hours after accept, an email reminder job is scheduled for 48 hours before that start, to both host and filmmaker.

### BR-B11 — Overlapping pending requests are reported, not blocked here

**Implemented.** `bookings.findOverlappingBookingRequests` finds other `Pending Host Approval` bookings on the same listing whose timeslots overlap or are identical. It does not change their status.

### BR-B12 — Calendar

**Implemented.** Payment success adds the booking to the listing calendar. Decline, either cancel path, and completion remove it. `bookings.delete` also removes a calendar entry and deletes the booking document. Caller authorisation on `bookings.delete` was not visible in the method body that was read. **Requires confirmation.**

### BR-B13 — Reviews are explicit flags

**Implemented.** `bookings.setHostReviewed` and `bookings.setFilmmakerReviewed` set booleans and emit Intercom events. Creating the review content is a separate reviews method.

### BR-B14 — Payout made is an admin mark

**Implemented.** `booking.setPayoutMade` requires role `admin` (the call uses `Roles.userIsInRole(this.userId, "admin")` without `Roles.GLOBAL_GROUP`). **Requires confirmation** that the role scope matches how `admin` was assigned (`GLOBAL_GROUP` at bootstrap).

## Messaging

### BR-M1 — Threads belong to a listing conversation

**Implemented.** `Messaging.createThread` sets status `"Active"`. Archive and un-archive update `hostHasArchived` or `filmmakerHasArchived` depending on the caller path in `message_threads.js`.

### BR-M2 — Chat SMS default

**Implemented.** On user create, `settings.send_chat_messages_to_cellphone` is true.

## Reviews and mail

### BR-R1 — Review notification delay

**Implemented.** `scheduleFilmmakerHasBeenReviewedEmail` and `sendHostHasBeenReviewedNotification` schedule work for 48 hours after the call.

## Jobs

### BR-J1 — Past jobs are dropped on startup

**Implemented.** Startup deletes queue documents whose `date` is before now, then sets every remaining job back to pending with `serverId: null`.

### BR-J2 — Daily pickup

**Implemented.** `node-cron` expression `0 8 * * *` with no timezone. The log text says "once a day at 8am". Which timezone the process uses is the server default. **Requires confirmation.**

### BR-J3 — A failed job is not retried

**Implemented.** The failure callback logs and does not remove or reschedule the job. The source comment says retry behaviour should be defined.

## Legal copy

**Documented** as in-app documents, not as code rules:

- Terms of Use, Acceptable Use (shown on the Terms of Use tab), Privacy Policy, Terms of Service at `/legal`.
- Location agreement component exists (`LocationAgreement.jsx`) and is rendered through `BookingAgreementFactory` at `/booking/agreement/:bookingId`.
- Sexual harassment policy is a component. It is mounted on `/testing`.

The Terms of Service text says promotional offers may change amounts charged, at Everyspace's discretion. That sentence is **Documented** in the component. The code's always-on fee waiver is the only promotion implemented in `bookings.create`.

## Rules that must not be treated as confirmed policy

| Topic | What the code does | Why it needs a person |
|---|---|---|
| Filmmaker 5% fee | Calculated, then fully discounted on every booking | May be a leftover promotion |
| Cancel inside 48 hours | No refund call | Not spelled out outside the branch |
| `terminationDate` + 7 days | Stored only | Purpose unknown |
| Host role method | Any caller who can invoke the method can toggle `host` | Likely unintended |
| Admin listings publication | Condition uses an undefined `userId` | Likely unintended |
| Payout | A stored number and an admin checkbox | No payment rail found |
