Acceptance criteria
Each criterion is testable against the API and, where a screen exists, against the portal. Amounts are USD cents.
Pricing and accounts
AC-1. Server price. Given a package priced at 20000 cents, when a customer submits a booking with any other amount, then the order subtotal is 20000, the currency is USD, and the provider checkout uses 20000 cents.
AC-2. Price snapshot. Given a paid order item at 20000 cents, when an administrator changes the package price, then the stored order item remains 20000 cents.
AC-3. Normalized email. Given a new booking for Ada@Example.com, then the stored email is ada@example.com. A second booking for ada@example.com does not create another user.
AC-4. New account setup. Given a verified payment for an email with no account, then the user is PENDING_SETUP, one unused setup token exists, and an email job is queued. Opening the link twice succeeds only the first time.
AC-5. Existing account. Given an active customer, when that person books again, then the booking's customer_id is the existing user and no setup token is created.
Payment and booking approval
AC-6. Redirect is not payment. Given a checkout session, when the browser opens the success URL before any verified webhook, then the order stays PENDING_PAYMENT and the booking stays PENDING_PAYMENT.
AC-7. Verified payment. Given a signed Stripe success event for the order total, when the API processes it, then the order is PAID, the booking is PENDING_APPROVAL, and the photographer has a notification.
AC-8. Duplicate webhook. Given AC-7 has been applied, when the same provider event id is delivered again, then the response succeeds and the order, booking, payment, and reward rows are unchanged.
AC-9. Approval. Given a booking in PENDING_APPROVAL with a paid order, when the photographer approves it, then the booking is CONFIRMED, approved_by and approved_at are set, and an audit row is stored.
AC-10. Rejection refund. Given the same paid booking, when the photographer rejects it, then the booking is REJECTED, the order becomes REFUND_PENDING, and a job calls the provider refund for the captured USD amount. When the refund event is verified, the order is REFUNDED.
AC-11. Approval guard. Given a booking still in PENDING_PAYMENT, when an administrator calls the decision endpoint, then the API refuses the decision.
Gallery access
AC-12. Private until approved. Given a gallery whose publication is PUBLISHED and whose access is PENDING_APPROVAL, when the owning customer requests a photo, then the API returns no bytes and no signed URL.
AC-13. Approval grants access. Given that gallery, when the photographer approves access, then access is APPROVED, a gallery_access_reviews row records the decision, and the customer receives an email. A following download request returns a signed URL that expires within 5 minutes.
AC-14. Revocation. Given an approved gallery, when the photographer revokes access, then a new download request is refused. A URL signed before the revocation still works until its own expiry.
AC-15. Ownership. Given customer A's approved gallery, when customer B requests it, then the API refuses the request.
AC-16. Rating. Given an approved ready photo, when the owner submits a rating of 5, then one rating is stored and the photographer is notified. A second rating from the same customer for that photo is refused. A rating of 0 or 6 is refused.
Rewards
AC-17. Five percent example. Given a 500 basis-point policy and an eligible subtotal of 20000 cents, when the reward is calculated, then the reward is 1000 cents, which is $10.00.
AC-18. Half-up rounding. Given a 500 basis-point policy and an eligible subtotal of 1010 cents, when the reward is calculated, then the reward is 51 cents.
AC-19. Self-referral. Given a customer submitting their own referral code, then no referral row is created.
AC-20. One attribution. Given a referred customer who already has a referral, when a different code is submitted, then the original referrer remains the only referrer.
AC-21. Spendable only after confirmation. Given a qualified reward still PENDING, when the referrer checks out, then that reward cannot reduce the total. When the qualifying booking becomes CONFIRMED, then a positive ledger credit exists and the balance includes it.
AC-22. Concurrent redemption. Given an available balance of 1000 cents, when two checkouts each try to redeem 1000 cents at the same time, then one redemption is stored and the other is refused. The ledger sum is never below zero.
AC-23. Refund reversal. Given an available unredeemed reward of 1000 cents, when the qualifying order is fully refunded, then a ledger reversal of -1000 cents is inserted, the original credit row is unchanged, and the spendable balance is 0.
AC-24. Ledger immutability. Given a ledger row, when an update or delete is attempted in PostgreSQL, then the database rejects the statement.