Skip to content

Venue Module Backend

Related documentation: Backend Modules · Venue Module Backend API · Data ownership · Access Control · Frontend Venues

Venues accepts Auth-issued X-Service-Token credentials additively and can use them for Artists and Promoters calls behind S2S_TOKENS_ENABLED; see the canonical service-token contract.

  • Backend-Kisum-Venues now exists as a real Go/Fiber service with PostgreSQL DI, JWT + x-org access enforcement, internal API-key routes, repository-backed MVP handlers, and a production-intended legacy sleeping-venue importer.
  • The current runtime already implements venues, spaces, availability blocks, bookings, customer overlays, and internal Base-facing venue reads.
  • Spaces include structured operating profile fields: capacity models, setup/load-in/load-out buffers, pricing defaults, amenities/equipment, accessibility/age/curfew/sound/technical notes, media/floorplan metadata, and marketplace publish/profile data.
  • The current runtime also exposes Admin-facing internal venue CRUD, venue-type, sleeping-inventory, takeover-approval, takeover-audit, reconciliation, and activation endpoints so Admin can use Venue as source of truth without direct DB access.
  • Phase 7 (reporting) is live. Four /api/v1/reports/* endpoints back the same query contract (fromAt, toAt, bucket, venueId, spaceId, businessUnitId, marketCompanyId, marketPersonId): utilization (pure Venue truth), revenue (Venue deposits merged with Finance-derived invoiced/paid), profitability (Finance income − Finance bills), customers (Venue bookings grouped by Artists party id, optionally enriched with Finance totals). When the Finance HTTP client is unconfigured, reports return financeLinked = false with an explicit financeMessage — never silent zeros. Aggregations live in internal/venue/repositories/reports_postgres.go (ad-hoc SQL, no materialized views).
  • Phase 6 (events + operations) is live on the Venue side. Events are persisted as references to the canonical Promoter-owned event (promoterEventId) or as placeholders for off-platform promoters (isPlaceholder = true) with a reconciliation path via POST /api/v1/events/:id/link-promoter. Operations checklists, reusable templates, readiness state, and event/space assignment are all persisted.
  • Phase 4 commercial routes (deals, contracts, deposits, event finance-visibility) are repository-backed on the Venue side. Real external Adobe Sign upstream sync and the deeper Finance handoff (booking obligations, settlements, profitability source of truth) are still pending. Deposit invoice sync is live through Finance income machine mode; reporting read-path is live through GET /api/income and GET /api/bills.
  • Pending: real Adobe Sign upstream, deeper Finance handoff beyond deposit invoices + reports read-path, Venue HTTP client to call Promoters GET /internal/events/:id (resolver live on Promoters since 2026-05-27), Finance-side aggregation endpoints so reports don’t have to pull every row.
  • SQL migrations: tracked in venues_schema_migrations (pkg/sqlmigrate, cmd/migrate, make migrate-*). Apply explicitly via make migrate-up / migrate-up-one — not on API startup. migrations/destructive/ is never auto-run. DBs that already ran manual psql files must make migrate-mark-all once before applying new migrations.
  • This page documents the enforced module boundary plus the current runtime direction.

Backend-Kisum-Venues is the domain backend for the venue module.

It exists so Kisum can manage a venue as an operating business, not only list venues or expose legacy CRUD.

Venue module responsibilities include:

  • venue master records
  • space records
  • space-scoped availability and booking workflows
  • multi-space booking support
  • venue-side customer overlays on top of Market parties
  • venue-rental contract lifecycle state
  • Adobe signature orchestration state
  • deposit workflow metadata
  • venue operational readiness
  • venue and per-space reporting
  • imported sleeping venue inventory

Current v1 product rules that the backend must support:

  • 1 company = 1 venue
  • 1 venue = many spaces
  • a space behaves like a lightweight BU-style operational scope
  • one booking may include one or many spaces
  • whole-venue rentals are valid
  • managers can edit operational details
  • admin / tenant superadmin finalize approval-sensitive actions
  • Finance remains the source of truth for financial persistence

Future multi-venue ownership must stay possible, but it is not the current tenant contract.

  • venue records
  • imported sleeping venue inventory before takeover
  • space records
  • venue type catalog imported from legacy Mongo
  • availability blocks
  • booking records (incl. optional eventId / promoterEventId linkage carried from creation through confirmation)
  • venue-side event references / placeholders (NOT the canonical promoter event — see Promoter ↔ Venue event linkage below)
  • venue-side deal structures
  • contract workflow state
  • signature orchestration state
  • venue-side deposit metadata
  • customer overlay data
  • operational templates, checklists, tasks, and readiness state
  • venue reporting views
  • user identity
  • sessions / JWT issuance
  • memberships
  • company / business-unit master data
  • package / entitlement truth
  • canonical Artists party master data
  • canonical geo master data
  • invoice / settlement / ledger truth
  • canonical event records — those live in the Promoter module; Venue stores only references or placeholders for off-platform promoters
  • external directory data — OpenStreetMap and similar upstreams are consulted live for venue discovery; Kisum does not mirror or bulk-import them (see External venue directory below)

When a venue search finds little or nothing in Kisum, Venue falls back to an OpenStreetMap geocoder so the operator gets candidates instead of a dead end (the existing GET /api/v1/venues/sleeping, which gains an optional meta.external block — no new endpoint, and data keeps its current shape).

The boundary is deliberately narrow:

  • An external record is a suggestion, never a venue. It becomes a Kisum row only when a person explicitly picks it. Nothing is bulk-imported.
  • How it becomes a row depends on who is asking. A venue operator claiming their business goes through creation requests — proof of ownership plus admin approval. A promoter naming where a show happens goes through POST /internal/venues/from-external — no proof, no approval, because ownership is not the question being asked. That path creates the row sleeping and claimable, so the real operator can still claim it later through the takeover flow.
  • Kisum does not become a mirror of OpenStreetMap. Bulk loading (Overture/Overpass) was evaluated and rejected: it would fill venues with non-bookable POIs and create a duplicate-matching burden larger than the value.
  • Lineage is persisted, not implied. Approved venues carry external_source, external_source_id, and external_source_attribution (migration 000040). OSM is ODbL, so attribution has to survive into the database and out to any surface that displays the venue.
  • External capacity is never authoritative. It is carried as an advisory hint for a human to confirm, because OSM capacity figures usually describe a seated or sports configuration rather than a concert one.
  • The failover cannot break search. An upstream outage degrades to an explicit externalStatus: "unavailable" alongside intact internal results.

Off by default (VENUE_EXTERNAL_DIRECTORY_ENABLED). Contract detail lives in the Venue API page.

Canonical event truth lives in the Promoter module. Venue stores only a reference row in venue_events so finance-status, operations checklists, and reporting can hang off something local without forcing every event consumer to round-trip through Promoter.

Every venue_events row is exactly one of:

  • Real promoter referencepromoter_event_id is set, is_placeholder = false. The Venue row mirrors a canonical promoter event.
  • Placeholder for off-platform promoteris_placeholder = true, promoter_event_id is null, plus placeholder_promoter_name (and optionally placeholder_promoter_email, placeholder_notes). The promoter has not joined Kisum yet; this row holds the venue-side operational state until reconciled.

Three creation paths populate venue_events:

  1. Explicit POST /api/v1/events — strict: must carry exactly one of promoterEventId or placeholder=true plus placeholder promoter context.
  2. Booking confirmation sync (SyncBookingEvent) — resolves in order booking.eventId (existing linked row) → booking.promoterEventId (upsert by (company_id, promoter_event_id)) → placeholder shadow keyed on booking_id. The resolved id is back-linked onto the booking row.
  3. Placeholder reconciliation (POST /api/v1/events/{id}/link-promoter) — promotes a placeholder to a canonical promoter reference once the previously off-platform promoter joins Kisum, mirroring the sleeping-venue takeover pattern.

Rules:

  • PATCH /api/v1/events/{id} may update venue-side operational fields (title, description, status, start/end, spaceIds, placeholder context on placeholder rows) but never mutates promoter_event_id or is_placeholder. Promotion only happens via link-promoter.
  • The (company_id, promoter_event_id) pair is unique when promoter_event_id is non-null — a real promoter event can be referenced by at most one Venue row per company.
  • Promoters exposes GET /internal/events/:eventId (machine key) since 2026-05-27 — accepts publicEventId UUID or Mongo _id. Venue Phase 3 will call it before persist; until then Venue trusts caller format for promoterEventId.
  • identity
  • sessions
  • JWTs
  • company memberships
  • business-unit memberships
  • effective module / permission resolution
  • companies
  • business units
  • commercial catalog
  • subscriptions
  • entitlements
  • canonical counterparties
  • canonical contacts / people
  • country master data
  • city master data
  • obligations
  • invoices
  • payments
  • settlements
  • accounting integration truth

Practical rule:

  • Venue orchestrates venue workflows
  • Finance persists finance truth

Admin venue management must flow through:

  1. Frontend-Kisum-Admin
  2. Backend-Kisum-Admin
  3. Backend-Kisum-Venues

Rules:

  • Backend-Kisum-Admin stays the browser-facing BFF
  • Backend-Kisum-Venues stays the runtime source of truth
  • Admin must not read Venue Postgres directly
  • the integer venues.id is the canonical venue identity everywhere (it was venue_id before migration 000042)
  • sleeping venue takeover approval, audit, reconciliation, and activation are Venue-owned workflow state surfaced through Admin

Venue must not duplicate party master data.

Recommended and current target rule:

  • bookings, contracts, and overlays reference Market company/person ids when possible
  • Venue stores only local overlay data such as:
    • notes
    • preferred terms
    • risk
    • relationship history
    • venue-specific performance context
    • local operational contacts

Venue depends operationally on Finance and should be treated as finance-linked.

At the product level:

  • a company using Venue Management should also have Finance enabled
  • venue-management users should get the Finance visibility required for deposit and settlement workflows

At the persistence level:

  • Venue may own deposit expectations and operational status
  • Finance owns invoice/payment/settlement/accounting truth
  • Current runtime detail: Venue deposit create/update now calls Finance POST|PATCH /api/income in machine source=venue_deposit mode, stores the returned Finance id locally, and should read paid-state back from Finance

Venue owns the canonical venue-rental contract record and its operational lifecycle:

  • draft creation
  • template selection
  • placeholder resolution
  • signer assignment
  • send for signature
  • signature progress
  • signed / declined / expired state
  • attachments and addenda

Adobe Sign is backend-only.

The frontend must never hold Adobe credentials or call Adobe directly.

Tenant-scoped requests require:

  1. Authorization: Bearer <JWT>
  2. x-org: <company_id>

Internal service-to-service routes may use:

  1. X-Internal-API-Key: <VENUE_INTERNAL_API_KEY>

Required fail-closed direction:

  • JWT validation
  • company context enforcement
  • venue module gate
  • granular venue.* permission checks
  • scoped visibility for space-level operations

Current stack:

  • Go
  • Fiber
  • PostgreSQL
  • pgxpool
  • migration-driven schema

Current route namespaces:

  • /api/v1/* for tenant/user routes
  • /internal/* for service-to-service reads

Current implemented route families:

  • venues
  • spaces
  • booking slots (per-space fixed windows + per-slot price; promoter bookings use bookingDate + slotIds)
  • availability blocks
  • bookings (with optional eventId / promoterEventId linkage; slot-based create derives startAt/endAt/totalPriceCents; PUT /api/v1/bookings/:id full operational edit; GET /api/v1/bookings/:id/counterparty Artists-enriched counterparty)
  • customer overlays
  • deals, contracts (with local signature adapter), deposits (with Finance income sync), event finance-status
  • events (incl. POST /api/v1/events/:id/link-promoter for placeholder reconciliation)
  • operations checklists + reusable templates (incl. event/space assignment and readiness state)
  • reports (utilization / revenue / profitability / customers — Venue aggregations + optional Finance HTTP enrichment via GET /api/income and GET /api/bills)
  • internal venue list/detail/inquiry
  • internal Admin venue CRUD, venue-types, sleeping-inventory, takeover-approvals/audit/reconciliation, activation

Current pending route families:

  • (none for v1; pending work is upstream — real Adobe Sign, deeper Finance handoff, optional Finance-side aggregation endpoints)

The current migration direction includes a hard import path from legacy Mongo.

Imported collections:

  • kisum_data.venues_type
  • kisum_data.venues

Legacy venue types import into the singular Postgres table:

  • venue_type

Key model:

  • venue_type.venue_type_id
    • numeric type id from Mongo
    • primary key

Venue rows link through:

  • venues.venue_type_id
    • FK to venue_type(venue_type_id)

Imported venue inventory lands under the platform tombstone company (00000000-0000-0000-0000-000000000001 / [Deleted organization]), not a real tenant company.

Imported records are marked:

  • ownership_status = sleeping
  • is_sleeping = true
  • is_claimable = true

Compatibility keys preserved on venues:

  • import source name -> external_source
  • the source record’s own id -> external_source_id
  • Mongo unique_id -> unique_id when present
  • the pre-migration TEXT uuid -> uuid

Current DB uniqueness rules:

  • id is the primary key (integer, sequence-backed by venues_id_seq)
  • uuid is enforced unique, so references written before migration 000042 still resolve
  • (external_source, external_source_id) is enforced unique where both are set
  • unique_id is enforced unique on live rows (partial index, deleted_at IS NULL)

Migration 000042 renamed venue_id to id, renamed the old uuid id to uuid, merged source_system/source_mongo_object_id into the external_* pair, and dropped source_collection.

Migration 000043 made country_id, venue_type_id and normalized_name NOT NULL, dropped name_normalized / normalized_city / normalized_country, and rebuilt the column order (id, uuid, name, unique_id, slug, venue_type_id, country_id, city_id, status, company_id, business_unit_id, then the rest). Geo ids are resolved at write time from Artists over HTTP — there are deliberately no local countries/cities tables; the city/country/timezone text stays on the row for display.

Imported venue rows should keep raw compatibility values:

  • country
  • city
  • country_iso2

and also resolve canonical Market references when possible:

  • country_id
  • city_id

Important rule:

  • city and city_id may remain NULL
  • unresolved geo ids must remain NULL rather than guessed

This phase is import-only.

Takeover approval and activation are later workflow phases and are not part of the import command itself.

Backend-Kisum-Venues optionally caches successful GET JSON responses in Redis (REDIS_URL, default TTL 7 days). Keys are scoped by path, query hash, and x-org.

Write invalidation: Every POST/PATCH/PUT/DELETE purges Redis before and after the handler (after only on 2xx). When the path includes a venue id, delete all keys matching venues:*{venueId}* plus venue list routes (/api/v1/venues, /internal/admin/venues, /internal/venues, /internal/catalog/venues). No write-through — the next GET loads from Postgres. Other writes still clear x-org / anon buckets and path-specific keys. Admin BFF mirrors the same venue-id purge on admin:*{venueId}*.

Empty lists are not cached: Responses with "data":[] (or null) are never stored, so a first “no bookings” read cannot stick after a later create.

Bypass (debug / support): Send header x-redis: bypass or query ?live=true on GET to skip read cache. Promoters BFF forwards x-redis when the browser sends it.

Product note: Promoter marketplace Calendar tab shows operator availability blocks (rose) plus the promoter’s own booking inquiries for the selected space (violet). Venue operator blocks do not include promoter inquiries until the venue accepts and materializes holds. Full inbox: Promoters My venue bookings; operator queue: venue Bookings + resource Calendar.

Booking slots + commercial packages (2026-06-29)

Section titled “Booking slots + commercial packages (2026-06-29)”

Venue operators define fixed bookable windows per space (e.g. Slot 1 08:00–13:00, Slot 2 14:00–22:00). Pricing lives on commercial packages per slot, not flat price_cents on the slot row (removed in migration 000031). Each slot has ≥1 package; exactly one default; packages may be public (marketplace) or internal (quote-only). Reusable templates clone onto slots.

Promoters do not pick arbitrary start/end times — they send bookingDate (YYYY-MM-DD) + slotIds + slotPackageSelections ([{ slotId, packageId }], one public package per slot) on POST /api/v1/bookings (Promoters BFF: POST /api/venues/bookings). The Venues service resolves slot templates in the venue timezone, snapshots package terms on the booking (slot_package_selections JSONB + termsVersionHash), sets indicative_price_cents (fixed rent only at inquiry), stores start_at/end_at for calendar/conflict checks, and sets total_price_cents = indicative_price_cents for backward compat. Confirmed bookings never mutate snapshots; GET booking may flag drift if live package terms changed.

Post-event settlement (manual sales inputs v1) calculates final rent from snapshotted terms + versus groups; POST /api/v1/bookings/:id/settlement/submit calls FinanceHandoff.SubmitSettlement (noop v1 — documented payload contract for Finance Phase 7).

SurfaceRoute / UI
Venue admin — slotsGET/POST/PATCH/DELETE /api/v1/booking-slots — Settings → Spaces → clock icon (time/status only)
Venue admin — packagesGET/POST /api/v1/booking-slots/:slotId/packages, PATCH/DELETE /api/v1/booking-slot-packages/:id, POST …/from-template/settings/spaces/[spaceId]/slots/[slotId]/packages
Venue admin — templatesGET/POST/PATCH/DELETE /api/v1/commercial-package-templates/settings/commercial/templates
Venue admin — settlement`GET
Promoter readGET /api/venues/booking-slots?spaceId=&venueCompanyId=&visibility=public (public packages embedded per slot)
Promoter bookPOST with { bookingDate, slotIds, slotPackageSelections, spaceIds, venueId, … }

Migrations: 000020_venue_booking_slots.sql (slots + booking window columns); 000021_venues_currency.sql (canonical venues.currency); 000031_slot_commercial_packages.sql (templates, slot packages, booking snapshots, settlements; backfill Standard package; drop slot price_cents).

Venue currency. Commercial pricing currency is not operator-editable. Each venue row stores canonical venues.currency (ISO 4217). All package term amounts and settlement calculations use venue currency.

The booking surface gained five hold tiers, a full operational edit endpoint, and an Artists-enriched counterparty read. See the API page for the exact request/response shapes.

Five hold tiers. Booking status now supports hold_option_1 through hold_option_5 (added hold_option_3/4/5). The venue_bookings.status column is free-text TEXT with no DB CHECK, so no migration was needed. Holds 3–5 occupy a window the same way holds 1–2 do: the occupancy status IN (...) lists in HasAvailabilityConflict (internal/venue/repositories/postgres.go) and the reporting aggregations (reports_postgres.go) include all five hold tiers.

Full booking edit — PUT /api/v1/bookings/:id (guard venue.booking.manage). Edits operational fields only: marketCompanyId, marketPersonId, title, spaceIds, startAt, endAt. Deliberately excludes status (owned by PATCH), finance/event linkage, slots, and pricing. Re-runs the conflict check ignoring the booking’s own row. Terminal bookings (cancelled / closed) are frozen → 409.

Market counterparty lookup — GET /api/v1/bookings/:id/counterparty (guard venue.booking.view). Returns a Counterparty = the local venue-private customer overlay (name, partyType, riskLevel, notes) merged with the canonical Artists/Market party detail (company + optional person: legalName, country, city, email, phone, website, contactName, raw attributes). The new MarketPartyResolver.GetPartyByID calls Market GET /companies/{id} or /people/{id} and is best-effort: when Market is unconfigured or returns 404 it yields nil and the response sets marketLinked:false — the read never fails. Wires Market enrichment into the booking read path. No migration.

When working in Backend-Kisum-Venues:

  • do not duplicate canonical Artists party records
  • do not redefine Finance truth in Venue
  • do not weaken auth or x-org rules
  • keep /internal/* internal-only
  • treat sleeping-venue import compatibility as real product behavior, not throwaway migration code
  • keep docs explicit about what is live versus still target-state

Promoter-originated venue bookings carry promoter event, BU, acting-user, and scope metadata. Venue stores it for Promoters BFF filtering; Venue operator authorization remains separate.