Functional requirements
Requirements use the identifiers below in stories, acceptance criteria, and tests.
Catalog and pricing
| ID | Requirement |
|---|---|
| FR-1 | The public catalog lists active services and active packages. |
| FR-2 | Each package has a name, a duration in minutes, and a price in USD cents. |
| FR-3 | The server computes every order total from the stored package price. The client cannot submit a price that the server trusts. |
| FR-4 | An order item stores the package name and unit price at purchase time. Later catalog edits do not change that snapshot. |
Identity
| ID | Requirement |
|---|---|
| FR-5 | Email addresses are stored trimmed and in lowercase, and are unique. |
| FR-6 | A new payer is created as a customer in PENDING_SETUP. Verified payment sends a single-use account setup link. |
| FR-7 | An email that already belongs to an active customer must sign in. The system attaches the booking to that account and does not send account setup. |
| FR-8 | Passwords are stored only as Argon2id hashes. Refresh tokens are stored as hashes. Access tokens are short-lived signed tokens and are not stored. |
| FR-9 | Password recovery uses a single-use hashed token. |
| FR-10 | An administrator must pass multi-factor authentication before using administrative actions in production. |
Booking and payment
| ID | Requirement |
|---|---|
| FR-11 | Creating a booking also creates one order in PENDING_PAYMENT and one booking in PENDING_PAYMENT. |
| FR-12 | Checkout is created with Stripe in USD for the server-calculated total. |
| FR-13 | The success or cancel redirect never marks an order paid and never approves a booking. |
| FR-14 | A verified provider webhook is deduplicated with UNIQUE(provider, event_id). Replaying an event does not apply the payment twice. |
| FR-15 | A verified successful payment marks the order paid and the booking PENDING_APPROVAL, then notifies the photographer. |
| FR-16 | The photographer approves a paid booking into CONFIRMED or rejects it. |
| FR-17 | Rejecting a paid booking starts a full refund of the captured USD payment and moves the booking to REJECTED. |
| FR-18 | Cancelling a confirmed booking whose payment was captured starts the same full USD refund as a rejection. A completed booking is not refunded by this workflow. |
| FR-19 | Failed or abandoned payments leave the booking in PENDING_PAYMENT or mark the payment failed. The customer can retry. A retry adds a payment attempt on the same order. |
Gallery
| ID | Requirement |
|---|---|
| FR-20 | Photographs are uploaded to a private bucket. The database stores the object key, thumbnail key, and status. |
| FR-21 | A gallery has a publication status of draft, published, or archived, and a separate access status of pending approval, approved, or revoked. |
| FR-22 | Publishing a gallery does not approve access. Access stays pending until the photographer approves it. |
| FR-23 | Every view and download checks, on the server, that the caller owns the gallery, publication is PUBLISHED, access is APPROVED, and the photo is READY. |
| FR-24 | The API issues a short-lived signed URL only after FR-23 passes. The initial lifetime is 5 minutes. |
| FR-25 | Revoking access writes a review row, sets access to REVOKED, and stops new signed URLs. URLs that were already issued remain valid until they expire. |
| FR-26 | Each approval, rejection, or revocation appends a gallery_access_reviews row. The current status used for authorization stays on galleries. |
| FR-27 | A customer with approved access can rate each photo once, from 1 to 5, with optional feedback. The photographer receives a notification. |
Referrals and discounts
| ID | Requirement |
|---|---|
| FR-28 | Each active customer has one referral code. |
| FR-29 | A customer can be referred once. Self-referral is rejected. |
| FR-30 | The reward rate is the active policy's basis points. 10,000 basis points equals 100 percent. The allowed range is 0 through 10,000. |
| FR-31 | The eligible amount is the service subtotal after discounts. Taxes and travel are excluded. Refunded service amounts reduce the eligible amount. |
| FR-32 | The reward in cents uses integer half-up rounding: (eligible_cents * basis_points + 5000) / 10000. |
| FR-33 | One qualifying reward exists for a referral and qualifying order. The same order cannot fund two rewards. |
| FR-34 | A reward becomes spendable when its ledger credit is posted. That happens when the qualifying booking is confirmed and the order remains paid. |
| FR-35 | Redemption applies the available balance as a discount on a future booking, inside one database transaction, and cannot spend the same balance twice. |
| FR-36 | A refund posts a ledger reversal. Ledger rows are never updated or deleted. |
Notifications, jobs, and audit
| ID | Requirement |
|---|---|
| FR-37 | The system notifies the photographer when a payment is verified and when a customer rates a photo. |
| FR-38 | The system emails the customer when gallery access is approved. |
| FR-39 | Email delivery is tracked separately from the in-app notification. |
| FR-40 | Thumbnail generation, email sending, refund calls, and reward expiry run as retryable background jobs. |
| FR-41 | Administrative and security actions append an audit record. Audit rows are not updated or deleted. |
Authorization
| ID | Requirement |
|---|---|
| FR-42 | Customer routes require an authenticated customer and ownership of the booking, order, gallery, or rating. |
| FR-43 | Approval, publication, refund, policy, and audit routes require an administrator who has completed multi-factor authentication. |
| FR-44 | Gallery authorization is enforced by the API. Hiding a link in Next.js is not the control. |