Added
- Enterprise Venue & Seating (Phase 1): reserved seating for ticketed venues — pick-your-seat and best-available, with box-office control
PriceCategory(venue-scoped seat pools) lets a tier price each seat by its painted category — the standard theatre model, via acategory_pricesmap. An empty map means one flattier.pricefor the whole sector; the buyer's zone travels as aprice_category_idrequest parameter. Onuser_choicetiers the map must cover every painted category (a clickable-but-unpriced seat is refused at checkout); onbest_availabletiers the keys define which zones the tier may sell from.SeatHold— TTL cart holds with takeover-upsert acquisition, so a seat is never double-sold and an abandoned cart releases itself; holds report aconflict_reasondistinguishing "sold out" from "unavailable"EventSeatOverride— per-event box-office hold/kill of individual seats with a reason, without touching the venue layoutVenueSector.kindplus seatrow_label/row_order/adjacency_index, so rows sort and seat neighbours are known (adjacency drives best-available grouping)- Public seat-chart, availability, and hold endpoints; organization-admin
PriceCategoryCRUD (deletion refused while any tier prices against the category) and seat overrides undermanage_tickets - Seat-painting operations report which tiers they repriced and which they left unsellable;
?preview=truereturns the identical report without writing - Verified under load (hold storms, best-available herds, 40-way same-second purchase races): no double-sold seat, no deadlock, async checkout p95 1.13 s contended / ~100 ms uncontended
- Membership Subscriptions: recurring paid memberships, offline or via Stripe Connect
- Full lifecycle — subscribe, cancel (immediate or at period end), pause, resume, change plan (via Stripe Subscription Schedules), and revive a lapsed membership; price grandfathering with an optional staff force-migrate
MembershipSubscriptionPlan.max_subscriptionscaps concurrent subscriptions (counted live, slots reclaimed on cancel/expiry) andsales_status(OPEN/PAUSED) stops member self-service without archiving the plan; the public plan schema exposessales_statusand a computedsold_out- Dunning notifications on failed and 3DS-blocked renewals, refund-triggered auto-cancel, renewal reminders, a nightly Stripe reconcile, per-organization metrics (MRR normalized to monthly), and full audit history
- Membership Eligibility Pipeline: a 10-gate eligibility stack for joining an organization, mirroring the event pipeline, on both the free and the paid path
- Application state machine on
OrganizationMembershipRequest(PENDING/APPROVED/REJECTED/CANCELLED/COMPLETED) withGET /me/organizations/{slug}/join-eligibility,/apply, and/me/applicationslist/get/cancel; the legacy/membership-requestssurface is preserved /applyaccepts aplan_id(tier derived from the plan) and/subscriberuns the full gate stack, so a tier can be paid and gated by questionnaire or staff approval — previously an unshippable combination. Stripe activation settles the originating application to COMPLETED at the same moment the member goes ACTIVE.- Approved-but-unpaid applications older than 30 days are cancelled by a daily sweep
- Application state machine on
- Free and lifetime membership plans:
PeriodUnit.LIFETIME(non-renewing — never lapses, never reminded, counted as zero recurring revenue) andSubscriptionPaymentMethod.FREE(self-serve, no Stripe). Subscribing to a FREE plan activates the membership in one transaction and returnscheckout_url: null. Plan shape is validated on create and update: FREE ⇒ price 0 and LIFETIME; ONLINE ⇒ price > 0 and never LIFETIME. GET /api/organizations/{slug}/membership-tiers— public membership-tier listing with resolvedrequires_approvalandquestionnaire_id, nested active plans, andis_free. Previously only plans were public, so a gated tier with no plan was invisible to prospective members.- Granular event visibility settings:
Event.visibility_settings— a validated, typed JSON object letting organizers hide the attendee count (show_attendee_count), the capacity (show_capacity), and the guest list (show_attendee_list). Defaults reproduce previous behaviour exactly. - Spanish (
es) and European Portuguese (pt) locales — the API now serves six languages, each catalog fully translated organization_slugfilter on the public event list, so discovery can be scoped to an organization by slug instead of UUIDOrganizationQuestionnaireresponses now carrytiersandis_organization_default, so the admin can see which membership tiers a questionnaire actually gates (it previously reported "Not assigned" for every one)- Admin action to requeue a
FAILEDreferral payout — a failed transfer was previously a dead end with the referrer's money stranded make seedships three fully laid-out showcase venues (theatre, comedy club, music hall) with materialized seats, tiers in every seat-assignment mode, and sold tickets
Changed
- Breaking (
Event):address_visibilityandpublic_pronoun_distributionare no longer top-level fields onEventBaseSchema,EventEditSchema,EventCreateSchema, orTemplateEditSchema. Both live inside the nestedvisibility_settingsobject, the latter renamed toshow_pronoun_distribution. Values and defaults are unchanged. Note the asymmetry for old clients:TemplateEditSchemaforbids extra fields and returns 422, whileEventEditSchemasilently ignores them. Requires a coordinated frontend update. - Breaking (error responses): the declared 400 schemas across 49 endpoints now match what those endpoints actually return. 39 declarations were wrong — 26 endpoints declared
ResponseMessagewhile producing{detail}or an eligibility body, and 16 declared onlyValidationErrorResponse. Endpoints that can return more than one shape now declare a union (e.g.EventUserEligibility | ErrorDetail), rendered asanyOfin OpenAPI. No runtime behaviour changed — only the declarations — but generated clients will see new error types. - Breaking (ticket tiers): the
RANDOMseat-assignment mode is removed; existing rows are migrated toBEST_AVAILABLE. Clients sendingrandomwill be rejected. GET /events/{id}/pronoun-distributionno longer returns 403 to non-privileged callers — it returns 200 with an empty distribution and null totals, matching every other visibility gate. Hiding attendee counts also hides the pronoun distribution, since per-pronoun counts sum back to the head count.- Stripe Checkout return URLs for memberships now land on
/org/{slug}/membershipinstead of the organization landing page, removing a redirect hop immediately after payment - Beta Docker images can be published from a pull request by applying a label, with only one open PR able to claim the beta tag at a time
Fixed
- Attendee-visibility rebuilds are serialized per event with an advisory lock. Concurrent checkouts on the same event deadlocked in Postgres (observed five times under a 30-user load test), leaving stale visibility flags behind; any ticket rush could trigger it.
- Deleting an account no longer fails for anyone involved in a referral. A user who held a referral code, referred someone, or merely signed up with a code could not be deleted: the endpoint returned 200 and burned the token while the background task died on a
ProtectedError. The inactive-account sweep also isolates failures per user instead of aborting the whole run. GET /api/account/referral/stripe/verifyno longer returns a 500 when the stored Stripe account is inaccessible or has been deletedGET /api/telegram/botnameno longer leaks an HTTP session on every cache miss (the source of the recurring "Unclosed client session" errors in production logs)make seedno longer sweeps tickets it did not create. Seeding a database that already held bootstrap fixtures rolled random payment statuses over every ticket, cancelling some and silently un-sold-out the sold-out demo event.
Removed
Event.address_visibilityandEvent.public_pronoun_distributioncolumns (see the breaking note above)TicketTierRANDOMseat-assignment mode
Security
- GDPR export hardening: the personal-data export is now built from an explicit allowlist rather than auto-discovery, so every relation carries a deliberate include/exclude decision. Unmapped relations are denied by default, and a guard test keeps it that way as new models are added. Exports are now scoped more tightly to the requesting user's own data.
pymdown-extensionsbumped to 11.0.1 (CVE-2026-61632)pyasn1bumped to 0.6.4 (CVE-2026-59885 / CVE-2026-59886)
What's Changed
- feat(events): showcase venue seed data (theatre, comedy club, music hall) by @biagiodistefano in #717
- chore: fix glued .gitignore line by @biagiodistefano in #718
- fix(events): serialize per-event attendee visibility rebuilds to prevent deadlock under concurrent checkout by @biagiodistefano in #719
- chore(ci): weekly .test_durations refresh by @revel-release-bot[bot] in #738
- ci: label-triggered beta image publishing by @biagiodistefano in #742
- fix(deps): bump pyasn1 to 0.6.4 to resolve CVE-2026-59885/59886 by @biagiodistefano in #772
- fix(deps): bump pymdown-extensions to 11.0.1 to resolve CVE-2026-61632 by @biagiodistefano in #775
- feat(events)!: enterprise venue & seating — phase 1 (engine, holds, best-available, zone pricing, box-office overrides) by @biagiodistefano in #724
- fix(accounts): allowlist GDPR export relations; stop third-party leakage by @biagiodistefano in #799
- chore(ci): weekly .test_durations refresh by @revel-release-bot[bot] in #789
- fix(telegram): close the bot HTTP session in get_bot_name by @biagiodistefano in #816
- fix(accounts): referral Stripe verify no longer 500s on inaccessible connected account by @biagiodistefano in #817
- fix(accounts): account deletion no longer fails with ProtectedError for users with referral involvement by @biagiodistefano in #818
- feat(memberships)!: subscriptions + eligibility pipeline integration (supersedes #403, #416) — sale caps, pause, hardened Stripe sync by @biagiodistefano in #774
- feat(events): organization_slug filter on public event list by @biagiodistefano in #822
- feat(events): granular event visibility settings — hide capacity, counts, and guest list by @biagiodistefano in #825
- fix(api)!: honest error-response schemas + three unmapped error paths (#712) by @biagiodistefano in #824
- feat(accounts): admin requeue action for FAILED referral payouts by @biagiodistefano in #823
- feat(i18n): add Spanish (es) and European Portuguese (pt) locales by @biagiodistefano in #828
- refactor(events)!: migrate address_visibility and public_pronoun_distribution into visibility_settings (#793) by @biagiodistefano in #829
- feat(events): public membership-tier listing endpoint (#830) by @biagiodistefano in #835
- feat(events): expose tier usage on OrganizationQuestionnaire (#833) by @biagiodistefano in #834
- feat(events)!: membership eligibility Phase 2 — gate stack on the paid path (#831) by @biagiodistefano in #836
- feat(events): free and non-renewing membership plans (#832) by @biagiodistefano in #837
- fix(events): scope the seeder's global sweeps to its own objects (#840) by @biagiodistefano in #841
- fix(events): point membership Checkout return URLs at the membership route (#838) by @biagiodistefano in #839
- chore(release): v2.0.0 by @biagiodistefano in #842
New Contributors
- @revel-release-bot[bot] made their first contribution in #738
Full Changelog: v1.72.1...v2.0.0