# Business rules — filmspace (Everyspace)

Evidence date: 2026-10-15. Source: `git@bitbucket.org:overdrive/filmspace.git`, `main` at `f19c1dc`.

Rules below are taken from code or from copy in the repository. Each rule is marked **Implemented**, **Documented**, or **Inferred**. A rule that the code applies without a written product statement is **Inferred** from that behaviour, and the behaviour itself is **Implemented**.

No commercial policy document was found apart from UI legal copy, PDFs in `public/`, and the README upgrade list. Where the code is the only source, do not treat it as an approved policy until someone confirms it.

## Identity and access

### BR-001 — Password login needs a verified email

**Implemented.** If the login type is `password` and the user's profile email is not verified, login throws `validateLoginAttempt.userEmailNotVerified`.

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

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

**Implemented.** `user.banned` throws `validateLoginAttempt.userBanned`.

Evidence: same file. Admin method `users.all.settings.set` toggles `banned`.

### BR-003 — Registration field checks

**Implemented.**

- First name minimum 3 characters. Contact form first name minimum 3.
- Confirm password must equal password. Message: "The passwords do not match".
- Email must include a TLD.
- Mobile minimum 10 characters and `SimpleSchema.RegEx.Phone`. Default country code `27`.
- Bank account number and postal code, when set, must be digits. Message: "{{label}} can only contain digits".
- Newsletter defaults to subscribed.

Evidence: `imports/api/Users/both/schemas/users.js`.

### BR-004 — Roles

**Implemented.**

| Role | Scope | What the code uses it for |
|---|---|---|
| `admin` | global | `/admin` routes, listing status while `in-review`, sort rating, payout flag, several listing and user methods |
| `host` | global | Remembered page checks it. `toggleUserIsHost` adds or removes it and does not check the caller |
| `editor` | listing id | Required to open `/listing/:listingId/...` unless the user is a global admin. Granted to the creator in `listing.create` |

**Inferred:** "host" in the product sense is the listing's `createdBy`, not only the `host` role. Booking records store `hostId` from `createdBy`. A user can create a listing without the code first checking the `host` role.

### BR-005 — Bootstrap administrators

**Implemented.** On startup, if `dean@codeswop.com`, `regardt@codeswop.com`, or `hello@everyspace.co.za` has no user, the app creates one with password `admin`, marks the email verified, and adds the `admin` role.

Evidence: `imports/api/Admin/server/init/create_admin.js`.

This is a security defect in the repository, not a recommended operating rule.

### BR-006 — Dashboard requires login

**Implemented.** The `/dashboard` group redirects to `/login` when `Meteor.userId()` is missing. Listing create and edit do the same, and also store `redirectAfterLogin` for the host-listings group and for listing forms.

## Listings

### BR-007 — New listings are hidden

**Implemented.** `listing.create` sets `status: "inactive"` and `published: false`. Search and the featured list require `status: "active"` and `published: true`.

### BR-008 — Review lock

**Implemented.** If the listing status is `in-review`, `listing.status.set` throws unless the caller has global `admin`. Otherwise the caller must be the creator or an admin.

### BR-009 — Status vocabulary

**Implemented.** Comment in `listing.status.set`: "Possible statuses: in-review, active, inactive". Host dashboards filter on those three strings.

### BR-010 — Who may delete a listing

**Implemented** on the client method `listing.delete`: the caller must be `editor` on that id or a global admin. The server method of the same name checks admin or `createdBy`. Two methods share the name `listing.delete` (`imports/api/Listings/client/methods/listings.js` and `imports/api/Listings/server/methods/listings.js`). **Requires Confirmation** which body Meteor registers last.

### BR-011 — Pricing tiers stay contiguous

**Implemented** in `maintainValidPricingTiers`. The next tier's minimum is the previous maximum plus one. A tier whose minimum is greater than or equal to its maximum is removed. Uniform pricing keeps a single tier up to `maxAttendees`. Empty tiers get one default tier from 1 to `maxAttendees`.

### BR-012 — Listing payout preview is not the booking charge

**Implemented.** `calculateNetPayout` assumes 12 hours: `(price * 12) * (1 - discount/100)`, then subtracts 15% and VAT on that 15%. `PricingService` charges `rate * actual hours` and applies the percentage discount only when hours are greater than 8. The two formulas differ. **Inferred:** the 12-hour figure is a preview on the listing form, not the amount Peach captures.

### BR-013 — Calendar blocks

**Implemented.** A successful pre-authorisation adds the booking to the listing calendar. Decline, cancel, and completion remove it. `cleanCalendar` pulls calendar entries whose `start` is before the job's date, then reschedules itself 30 minutes ahead. On startup, existing `cleanCalendar` jobs are removed and a new one is scheduled.

### BR-014 — Search eligibility

**Implemented.**

- Listing must be active and published.
- Caller must pass a location centre and at least one activity, or the result is empty.
- Distance cap 45 km (`maxDistance: 45000`).
- Activity filter uses the parent activity of the first selected sub-activity.
- Exact attendees require `maxAttendees >= exact`. Ranges: `1 to 25` means max at least 1, `26 to 50` means max at least 26, `51 to 100` means max at least 51, `100plus` means max at least 100. These are minimum-capacity filters, not upper bounds.
- Price filters compare `price_min` and `price_max` on the listing.
- Styles, amenities, and subcategory ids use `$all`. A space tag filters `spaceTagIds`.

### BR-015 — Category tree is data

**Implemented** as collections `categories` and `forms_for_categories`. The wizard extends listing validation with the schemas for forms linked to the listing's subcategories. A BSON file in git lists category names. It is not loaded by application startup. **Unknown / Requires Confirmation** if that file matches production.

## Money

### BR-016 — Currency

**Implemented.** Peach checkout, capture, and refund send `currency: "ZAR"`.

### BR-017 — What the booking fee is

**Implemented.** `PricingService.getBookingFee` is `rate * hours`, rounded to cents. Hours are the sum of timeslot durations. Additional fees are a separate sum of `fee.price`. They are not part of the service-fee base.

Rate comes from the matched pricing tier. If `exactAttendees` is set and `attendees` is missing, the tier whose min/max contains that number is used. If the booking is created with a `threadId` and that thread has `bookingFormFields.customRate`, the custom rate replaces the tier price.

### BR-018 — Discount

**Implemented.** `setDiscount` stores `discount/100` only when hours are greater than 8. Otherwise the discount is 0. The discount amount is that fraction of the booking fee.

### BR-019 — Service fees and VAT

**Implemented.**

| Fee | Base | Rate | VAT |
|---|---|---|---|
| Filmmaker | booking fee | 5% (`ServiceFeePercentageEnum.Filmmaker` is `0.05`) | 15% of the fee |
| Host | booking fee | 15% | 15% of the fee |

Host payout = booking fee − discount + additional fees − host fee including VAT.

Guest total before the promotion line = booking fee + additional fees − discount + filmmaker fee including VAT.

### BR-020 — Filmmaker fee is then removed from the charge

**Implemented.** `bookings.create` sets `promotionDiscount` to the filmmaker fee including VAT, and sets `pricing.total` to `getTotal() - filmmakerFeeIncludingVat`. The comment in code is `PROMOTION_FEATURE`. Peach authorises and captures `pricing.total`.

**Inferred** from the README label US-8 "Zero Service Fees": this subtraction is that promotion. The host 15% is still deducted from payout. **Requires Confirmation** that the waiver is still intended. It is not a setting. It applies to every `bookings.create`.

### BR-021 — When money moves

**Implemented.**

| Event | Peach type | Amount |
|---|---|---|
| Guest submits a request | Pre-authorisation `PA` | `pricing.total` |
| Host accepts | Capture `CP` | `pricing.total` |
| Host declines, or the 3-day window ends | Reversal `RV` | Peach reversal of the pre-authorisation (no amount in the call) |
| Guest cancels while still pending | Reversal `RV` | same |
| Host cancels an approved booking | Refund `RF` | `pricing.total` |
| Guest cancels an approved booking more than 48 hours before the first timeslot start | Refund `RF` | half of `pricing.booking` plus `additionalFeesTotal` |
| Guest cancels an approved booking inside that 48 hours | no Peach call | booking still set to cancelled |

Failure results throw and, on the accept and decline paths, do not apply the status change that sits inside the success branch. `booking.generateBookingPDFs` is called after the success check on accept and decline, including when the code threw. **Inferred:** a thrown error prevents the PDF call because the exception leaves the method. That depends on Meteor method semantics.

### BR-022 — Host decision window

**Implemented.** `approvalExpiresDate` is now plus 3 days at the moment the pre-authorisation succeeds. The job `bookings.onHostApprovalTimeExpires` calls `bookings.hostDecline(bookingId, true)`. The cancellation message is "The request timed out". A separate job 24 hours later records an Intercom reminder only if status is still `Pending Host Approval`.

`bookings.setExpiresIn` can move that deadline. It is a Meteor method with no role check in the file. **Requires Confirmation** who is allowed to call it. It looks like a support or test tool.

### BR-023 — Guest cancellation refund share

**Implemented** for the more-than-48-hours case. Refund to the guest is `round(pricing.booking * 0.5 + additionalFeesTotal)`. The stored host payout on `refundPricing` is half the booking fee minus a new host service fee on that half (15% plus VAT). Filmmaker service fee on the refund record is copied from the original pricing, which may already be the waived figure.

**Inferred:** additional fees are refunded in full in this branch, and half the time-based booking fee is kept. Inside 48 hours the guest is not refunded by this method. **Requires Confirmation** against the legal PDFs. Those PDFs were not transcribed into rules here.

### BR-024 — Payout is a flag

**Implemented.** `booking.setPayoutMade(bookingId, payoutMade)` requires `admin` and sets the flag. No payment provider is called. **Inferred:** money to the host is paid outside this system.

### BR-025 — Booking reference

**Implemented.** `FS` + next bookings counter + `-` + `D/M/YYYY`. Receipt id is `REC` + next receipts counter + the same date pattern.

## Booking lifecycle

### BR-026 — Status strings

**Implemented.** Stored values, not the object keys:

| Key | Stored string |
|---|---|
| `PENDING_HOST_APPROVAL` | `Pending Host Approval` |
| `APPROVED` | `Host Approved Booking` |
| `CANCELLED` | `Booking Cancelled` |
| `SUCCESSFUL_COMPLETED_BOOKING` | `Booking Completed Successfully` |

Cancellation type is `Declined` or `Cancelled`.

### BR-027 — Completion

**Implemented.** `bookings.onBookingEndTime` sets `Booking Completed Successfully` and pulls `{ bookingId }` from the listing calendar. `endDate` is the end of the last timeslot, stored when the pre-authorisation succeeds. `terminationDate` is that end plus 7 days, and host or guest cancel overwrites `terminationDate` with now.

The completion job is scheduled on host accept, at `endDate`. It is not scheduled at payment time.

### BR-028 — Shoot reminder

**Implemented.** If the first timeslot starts more than 96 hours after accept, a reminder email to host and filmmaker is scheduled 48 hours before the start. Bookings sooner than that get no 48-hour reminder.

### BR-029 — Caller identity on booking actions

**Inferred** as a gap, not as an intended rule. `bookings.hostAccept`, `bookings.hostDecline`, `bookings.hostCancel`, and `bookings.filmmakerCancel` load the booking by id and call Peach. The portions read do not compare `this.userId` to `hostId` or `filmmakerId`. `bookings.create` does require a logged-in user and sets `filmmakerId` to that user. **Requires Confirmation** before describing this as safe.

## Messages

### BR-030 — Custom rate

**Implemented.** A custom fee stores a numeric `customRate` and `canOverrideMinHours` on the thread. The next `bookings.create` for that thread uses `customRate` as the hourly rate.

### BR-031 — Unread notifications

**Implemented.**

- Each new message replaces any pending `notifyOfNewMessage` for that thread and sender, then schedules one 5 minutes out.
- That job sends SMS and email only if `messages[messageIndex].recipientHasSeen` is still false.
- It then schedules `notifyOfRecentDiscussionUnreadMessages` 25 minutes later. The schedule description string says "30 minute".
- The 15:00 summary includes only users with `settings.send_chat_messages_to_cellphone: true`.
- The 5-minute and 25-minute jobs do not read that setting.

**Inferred:** the settings switch does not silence all chat SMS. **Requires Confirmation** if that is accidental.

Development does not call Twilio. `SMSNotifications.send` logs the body when `Meteor.isDevelopment`.

### BR-032 — Archive is per side

**Implemented.** Archiving sets `hostHasArchived` or `filmmakerHasArchived`. A new message clears both flags.

## Reviews and documents

### BR-033 — Review flags

**Implemented.** `bookings.setHostReviewed` and `bookings.setFilmmakerReviewed` set booleans and send Intercom events. They do not check booking status in the method body that was read.

### BR-034 — PDFs

**Implemented.** Accept, decline, and both cancel methods call `booking.generateBookingPDFs`, which removes prior documents for that booking id and regenerates receipts, invoices, and the agreement. Puppeteer opens the app's own receipt and invoice URLs.

## Copy that is not a coded rule

### BR-035 — Insurance

**Documented** only. `HowEverySpaceWorks.tsx` tells hosts there is "fuss-free R1 000 000 liability insurance". No collection, method, or fee implements insurance.

### BR-036 — Listing is free

**Documented** on the same component: "List in minutes, all for free". **Implemented** behaviour: creating a listing does not charge a card. Booking a listing does. The host fee in BR-019 still reduces payout.

### BR-037 — Legal documents

**Documented** as PDFs and React policy text. This file does not restate clauses from those PDFs. If a PDF disagrees with BR-021 or BR-023, that disagreement needs a person to decide which source wins. It was not line-checked in this pass.

## Job queue side effects

### BR-038 — Startup resets the queue

**Implemented.** On server start, every `job_queue` document is set back to pending and `serverId` is cleared. Jobs with `date` before now are removed. The 08:00 cron calls the same enqueue function. **Inferred:** a job that was due yesterday will not run after a restart. A job due later today can be scheduled twice if more than one server starts, because the code comments that "the first server" should pick jobs up, while the observe callback schedules a timeout on each process that sees a pending job. **Requires Confirmation** for multi-instance deployment.
