Skip to content

Frontend Promoters

The company profile is the Promoters-side organization access console. It is split into Business Units and Users & Access tabs.

Business Units owns the shared department structure used by Events and Finance. Names are read-only by default and require Edit before renaming.

Users & Access owns user invitations, Promoters-managed Auth catalog permissions, and assigning users to existing business units. The Members & access card has an inline switch: Members (active people) and Pending invitations (not joined yet — resend or revoke). Company admins can permanently delete a member from the member profile page (Delete user): they must type the member’s email to confirm. If that person has no other companies, their login is removed and Finance keeps their submitted invoices with name and email saved on the record. Invitation emails link to {FRONTEND_URL}/accept-invitation?token=…&companyId=… on the Promoters app (not Auth web). The page loads the invited email from Auth GET /auth/invitations/preview (read-only); accept calls POST /auth/invitations/accept. Password reset and login stay on auth.kisum.io. Finance assignments default to Submitter. Finance approval roles and limits are deliberately absent and are managed in Finance.

Promoters is the reference implementation of the persona access editor. Artists and Venues implement the same contract more simply — see Artists and Venues for the differences, and the Company Access Ownership Matrix for the canonical owner of each concern.

Two behaviours worth copying when the other apps are brought to parity:

  • Edit implies view, in both directions. Ticking an edit-style permission (create/delete/edit/manage/write) adds the matching .view; unticking a .view removes the edit permissions that depend on it. A whole-list normalizer cannot express this — the toggle has to know which box the user clicked.
  • Saving preserves grants owned by other apps. The member update in Auth replaces every module rule, so the payload must send back everything it read, including module denies, or the omitted grants are deleted.

Users & Access only presents Promoters-managed namespaces: promoter.*, venue_marketplace.*, allowed ai.*, allowed finance.*, and middle_agency.* when relevant. Promoters-side artist, booking, and venue actions stay visible as promoter.artist.*, promoter.booking.*, and promoter.venue.*. It does not expose persona-owner artist.* or venue.* permissions, and saving from Promoters preserves hidden grants owned by other apps.

Permission areas render as accordions with module-level and subsection-level bulk controls for Select all, Select all view, Select all edit, and Deselect all. Labels are only View or Edit: GET-style permissions are View, write/action permissions are Edit, and AI permissions are always View. Selecting edit/manage access also selects the matching view access when that permission pair exists.

Finance is intentionally only a module checkbox in Promoters. The user starts as Submitter from this screen. Invoice, bill, approval, approver, limit, and admin permissions are managed inside Finance and are not overwritten by Promoters saves.

The Edit company drawer edits logo, name, business ID, email, website, phone, country, and the full postal address (region, city, postal code, two street lines). The address saves through PUT /api/companies/:id/address; everything else through PATCH /api/companies/:id/profile. PUT /api/companies/:id remains 410.

Social links and company documents are displayed but not editable: Core registers document metadata only — there is no file-upload path for them — and social links support create and delete but no update. Both sections say so rather than offering controls that would fail.

The company detail read pulls addresses, social links and documents from Core. It previously returned hardcoded empty values, so an address written by Checkout at signup always showed as “Location not set”.

Related documentation: Promoters Module Backend · Promoters Integration · Frontend Applications

/messages follows the Kisum App design mockup’s WhatsApp-style layout: a flush two-pane split (344px conversation list on surface-card, thread on surface-shell), a sidebar header with the unread-total pill, a New button and a rounded search pill, inbox rows with colored initials avatars, per-row timestamps and unread pills, a thread header with the peer avatar and name, grouped bubbles (peer avatar on the first message of a run, time + read checkmarks on the last), and a pill composer with a circular accent send button. Presence (“online”) indicators, call/emoji/attachment buttons from the mockup are intentionally not rendered — there is no backing capability for them. The floating Messages panel keeps its compact layout and shares the same bubble components.

Help / Messages floating buttons (2026-08-01)

Section titled “Help / Messages floating buttons (2026-08-01)”

The bottom-right Messages and Help / Get Started floating buttons (HelpButton) are draggable. Users can drag the cluster anywhere on screen (for example when it covers the KisumAI chat send control on /ai/chat). Position is saved in the browser (localStorage key kisum.promoters.help-fab-position) and stays until the user moves it again.

Promoter events from Backend serialize with Postgres numeric id (not Mongo _id). The Promoters app resolves links with resolveEventId (id then _id) and getEvents / getEvent also copy id onto _id so older list/card code keeps working. Detail routes are /events/:id where :id is that numeric event id.

Tour event venue / time display (2026-08-01)

Section titled “Tour event venue / time display (2026-08-01)”

On /events/tours, linked event cards must not assume nested venue or time fields always exist. After Postgres serialize, event.venue may omit location, and event.time may be missing. Tour cards/tables/drawers use formatVenuePlaceLabel (name + optional city/country) and optional event.time?.start / ?.end so the tours list does not crash when city/country are absent.

The All events / Tours tab badges use the same date filter as the list (default upcoming). They must not count past shows when the page is on Upcoming. Events use URL type; tours use URL typeDate.

The dashboard’s booking-network section follows the design mockup’s 1.6fr/1fr grid: a Recent requests table (latest five outbound booking requests — artist, show date, fee, status — linking into /booking/requests), and a right rail with the KisumAI entry card (links to /ai/chat) and Pipeline by stage progress bars fed by the same live counts the previous KPI tiles used (open requests, awaiting decision, confirmed).

Frontend-Kisum-Promoters is implemented.

The repo README is not the current contract. Use the repo TODO/changelog and the live route tree instead.

This is the dedicated promoter persona app. It consumes:

  • Backend-Kisum-Promoters
  • Artists network BFF routes
  • Venues network BFF routes
  • Checkout/Core public catalog for billing flows

It coexists with the older broader Frontend-Kisum surface.

Promoters is only for the promoter persona. Artist and venue users must be sent to their own app.

LayerBehavior
Client (app-shell-layout)Derived from the active company’s access payload that GET /api/users/init already returns (useInitializeUserresult.wrongApp). No browser → Auth call.
Block UIWrongPackageScreen — link target is Auth packageAppUrl (Auth owns persona → URL map), carried through init’s access.packageAppUrl.
AllowAuth package: promoter, promoter module keys, and Core package types such as basic_promoter.
BlockKnown artist/agency and venue personas only — not empty/unresolved package (fail-open).
No active companyNo verdict — you cannot be in the wrong app before picking a company; the company picker shows instead.
Root layoutNo server-side package guard — avoids racing the x-org cookie and keeps auth/init visible in browser DevTools (same pattern as Artists).
  • new /booking/* information architecture
  • booking marketplace
  • event detail Booking tab (/events/:id/booking) — lineup-scoped avails + per-artist offer/request CTAs
  • booking agencies directory + detail
  • Agencies + artist Team gate (2026-08-12): /booking/agencies/** and artist Team tab require promoter.booking.agencies.view from Auth and must not appear in Core access.deniedPermissionKeys (restriction packages). Owner/admin bypass does not apply when the org has an active restriction (e.g. seeded agencies_contact). Global state: deniedPermissionKeys from /users/init.
  • shortlist list + detail
  • booking requests list + detail
  • booking offers list + detail (unified marketplace offersheet view) + holds/actions where applicable
  • confirmed bookings list + detail with activity/logistics/finance tabs
  • relationships list + detail
  • dashboard booking KPI tiles
  • /venuesAll Venues directory reads only Backend-Kisum-Venues PostgreSQL via Promoters BFF GET /api/venues (sleeping=all). Detail links go to /venues/marketplace/[id] (API detail: GET /api/venues/:id).
  • Directory filters sync into the browser URL so refresh/share keeps state, e.g. /venues?country=CL,ID,SG&type=Stadium&capacity=5000-20000&sort=capacity-desc&q=arena.
  • Country filter supports one or many ISO2 codes. The FE sends iso2=CL,ID,SG on GET /api/venues; Venues admin list filters server-side (repeated iso2= or comma list).
  • Marketplace browse: GET /api/venues/marketplace. Mongo is not used.
  • /venues — Used Venues points to bookings (Mongo related list gone)
  • /venues/marketplace
  • /venues/marketplace/[id]
  • /venues/bookings

Addon gate: sidebar Venues and all /venues/* routes require Core addon venue_marketplace in access.modules from user init (plus the usual promoter.venue.* permission keys). Without the addon, the menu is hidden and direct URLs redirect to /profile/billing — same commercial pattern as Finance (finance) and KisumAI (ai).

  • /profile/billing
  • company/profile surfaces — see Company profile above. Address is now self-serve; documents and social links remain read-only.
  • checkout deep links for add-ons and card capture
  • dashboard
  • vendors
  • workspace/tasks
  • AI/research
  • public event pages
  • market analysis / rankings / festivals

Vendor selection on create/edit lives only inside Lineup (artist fee / agency commission vendor). There is no separate bottom “event vendors” block. Lineup vendor picks still sync into event vendor links on save (PATCH /api/events/:id/vendor-events).

Workspace tasks (2026-07-21): Task documents stay in Promoters Mongo CRM, but people on tasks are Auth users (canonical UUID from PostgreSQL). The UI loads assignees from GET /api/tasks/members (Promoters BFF → Auth company memberships), not legacy /api/company-users. Create/update/list decorate assignee and author fields from Auth; do not expect Mongo users ObjectIds on new tasks.

Task permissions (2026-07-22): Optional per-task grants on create: can_edit, can_manage_users (assignees), can_delete. Stored as access_grants on the task document. Creator always has full control; assignees can view and comment unless granted edit. Permission pickers list only users already chosen as assignees. Enforced on PATCH/DELETE in Promoters BFF.

Task comment live updates (2026-07-22): After POST /api/tasks/:id/comment, Promoters publishes to Centrifugo channel task:{taskId}:comments (namespace task). The task drawer subscribes via NEXT_PUBLIC_CENTRIFUGE_URL and refetches comments on each message. Notification bell still uses user:{userId}.

Notification fetches need active company (2026-08-06): GET /api/notification is company-scoped (x-org). Both useNotifications and useCentrifugeNotifications wait for activeCompany before calling it. Starting on user id alone races cold load and returns 403.

Full broker operator workspace copied from Frontend-Kisum-Artists into Frontend-Kisum-Promoters at /middle-agent/* (separate app shell; buyer flows under /booking/* unchanged).

SurfaceRoute familyBFF prefix
Dashboard, listings, inbox, offers, calendar, commissions, reports, partners, channels, artists directory, contacts, open-requests/middle-agent/*/api/artists-network/middle-agent-workspace/*
Workspace onboarding/profile/middle-agent-workspacesame (profile PATCH)
Guest intake links (mint from operator desk)Opens Fullstack-Kisum-Public /form/middle-agent/[token]Public app BFF → Artists public intake API

Gate: Core middle_agency addon + Auth middle_agency.* + successful workspace profile fetch (useWorkspaceProfile). Sidebar Middle Agent entry when entitled. Back link: Back to promoter/dashboard.

Operator components live under src/components/middle-agent-operator/ (buyer components remain in src/components/middle-agent/).

Representative live route families:

  • /signin
  • /signup
  • /public/events
  • /public/events/[id]
  • /dashboard
  • /profile
  • /profile/companies
  • /profile/companies/[id] — company detail; [id] is Core profile slug when set (UUID still works; app redirects to slug). x-org / API calls use canonical UUID.
  • /profile/billing
  • /artists — directory list; card/table links prefer slug via artistDetailPath (from Promoters GET /api/artists BFF)
  • /artists/[id] — detail shell; [id] accepts slug or numeric id (Artists Postgres)
  • /artists/[id]/[tab] — overview / team / biography / analytics / shows / discography
  • /booking/marketplace
  • /booking/agencies
  • /booking/agencies/[id]
  • /booking/agencies/[id]/staff
  • /booking/agencies/[id]/staff/[staffId] — staff detail (roster / availability / tours); not top-level /staff
  • /booking/shortlists
  • /booking/shortlists/[id]
  • /booking/requests
  • /booking/requests/[id] — withdraw cancels request; cancelled rows can be hard-deleted
  • /booking/offers
  • /booking/offers/[id] — combined detail route; marketplace links carry ?source=marketplace so the page reads the neutral marketplace workflow directly. Detail copy follows the stored exclusive|middle_agent channel.
  • /booking/confirmed — lists status=confirmed per-show bookings from Artists GET /bookings (promoter buyer org scoped via buyer_core_company_id after agency accept)
  • /booking/confirmed/[id]
  • /booking/relationships
  • /booking/relationships/[id]
  • /venues/marketplace
  • /venues/marketplace/[id]
  • /venues/bookings
  • /vendors
  • /workspace
  • /market_analysis/*
  • /ai/chat
  • /ai/research
  • /middle-agent — operator deal desk (addon-gated; full parity with Artists middle-agent shell)
  • /middle-agent/listings, /middle-agent/inbox, /middle-agent/offers, /middle-agent/open-requests, …
  • /profile/middle-agent-workspace — workspace onboarding (organization_type, profile fields)
  • Guest intake — not hosted here; operator desk mints tokens via BFF → Artists backend; share URL uses NEXT_KISUM_PUBLIC_URL + /form/offer/[token] on Fullstack-Kisum-Public

The repo TODO still tracks follow-ups such as:

  • marketplace artist tab
  • some venue-booking detail follow-ups
  • some phase-specific UX refinements around filters, aggregates, and deep linking

Those should be marked TODO, not documented as absent from the app.

AWS Amplify hosting (frontend + Lambda adjunct)

Section titled “AWS Amplify hosting (frontend + Lambda adjunct)”

Frontend-Kisum-Promoters ships a Gen2 Amplify backend under amplify/ (eight Lambda functions with Function URLs: router, aiChat, aiN8N, aiLongOps, news, artistDemographic, artistPredictionv3, artistTrends — plus auth/storage; no Amplify Data / AppSync).

PhaseWhat deploysHow
BackendAll amplify/ CDK resourcesamplify.ymlnpx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID on each connected-branch build
FrontendNext.js SSR appSame amplify.ymlpnpm run build:production

Requirements

  • Amplify Console app connected to this repo/branch (prod app ID d1h8upn1gdp1fp, region ap-southeast-1).
  • App service role with AmplifyBackendDeployFullAccess (required for amplify.yml backend phase).
  • One-time cdk bootstrap in ap-southeast-1 per AWS account.
  • Env contract: amplify-variables.json / Amplify Console (NEXT_PUBLIC_FUNCTION_URL_*, auth, BFF URLs).
  • CloudFront at api-v3.kisum.dev (distribution E35RWLCSFHM5UN) is manual — not updated by pipeline-deploy. After a new backend stack or Function URL change, update CloudFront origins, amplify/functions/router/resource.ts, and Console env vars.
  • Lambda bundling: every amplify/functions/*/resource.ts must set bundling.depsLockFilePath: 'pnpm-lock.yaml' (repo is pnpm-only; do not point at missing package-lock.json). Missing this on routerFunction caused Amplify CI Failed to build asset function/routerFunction-lambda/Code.
  • No Amplify Data: defineBackend does not include data. Starter Todo / AI conversation AppSync routes were unused and caused CI CDKAssetPublishError on VTL assets (Query.getTodo.req.vtl). Product APIs are Function URLs only.
  • CDK staging bucket: Amplify Gen2 publish needs S3 cdk-hnb659fds-assets-<account>-ap-southeast-1. If it is deleted while CloudFormation CDKToolkit still lists it, CI fails with CDKAssetPublishError / AggregateError on random assets — recreate the bucket (job 551 recovered this way on 2026-07-10).

Canonical ops runbook: repo amplify/README.mdProduction deploy A–Z (from zero) (IAM, CI, run-amplify-get-lambda-urls.sh, run-amplify-update-cloudfront.sh for origins, run-amplify-update-cloudfront-behaviors.sh for path behaviors, env sync, delete old sandbox stacks, tests).

Runtime wiring

  • Prefer NEXT_PUBLIC_FUNCTION_URL_ROUTER (https://api-v3.kisum.dev) for browser and Next.js server routes that hit Lambdas — CloudFront injects X-Origin-Key at the edge. Server routes also send x-auth, x-org, and x-origin-key via src/libs/api-v3-proxy.ts.
  • longOpsFetch uses api-v3 path /api/ai/longops/* with JWT + x-org; aiLongOps strips that prefix before proxying to Promoters BFF.
  • newsFunction calls MusicData with machine auth only (not the viewer JWT). Phase 2 deploy 1: when S2S_TOKENS_ENABLED=true, mints Auth service token as promoters → audience musicdata (X-Service-Token); otherwise falls back to static MUSICDATA_INTERNAL_API_KEY. Console secrets: AUTH_SERVICE_SECRET (same as Promoters BE) + optional static key; never commit secrets in amplify/lambda-env.ts.
  • artistDemographic, artistPredictionv3, artistTrends, and aiLongOps verify Kisum JWT + x-org at the Lambda edge (Auth JWKS) before calling Promoters BFF; env: AUTH_JWT_VERIFICATION, AUTH_JWT_ISSUER, AUTH_JWT_AUDIENCE (see amplify-variables.json).
  • aiChat maps viewer paths /ai/chat → upstream POST /chat, /ai/sessionsGET|DELETE /sessions on AI_CHAT_UPSTREAM_URL. Lambda converts viewer x-auth to upstream Authorization: Bearer ….
  • Production KisumAI path: Next /api/ai/*NEXT_PUBLIC_FUNCTION_URL_ROUTER (https://api-v3.kisum.dev) → router → aiChat Lambda → AI_CHAT_UPSTREAM_URL. Local dev still bypasses api-v3 and calls AI_CHAT_UPSTREAM_URL directly (NODE_ENV=development). Do not set AI_CHAT_UPSTREAM_URL to api-v3.kisum.dev — that is the CloudFront proxy, not the AI backend.
  • The CloudFront default behavior (*) fails closed. It used to target NewsOrigin, so a path with no matching behavior returned the MusicData news payload with HTTP 200 instead of an error. On 2026-07-28 production GET /ai/sessions and POST /ai/chat had no behavior and silently returned { "excerpt": [...] }; the Promoters BFF surfaced it as 502 Chat history upstream returned news data instead of sessions. Dev was unaffected because NODE_ENV=development bypasses api-v3 and calls AI_CHAT_UPSTREAM_URL (https://api.kisum.io/ai) directly.
  • Two layers now stop it, neither a single point of failure: (1) amplify/cloudfront-functions/api-v3-not-found.js, a cloudfront-js-2.0 viewer-request function on the default behavior, returns 404 {"error":"Not Found"} without reaching an origin — the default still names NewsOrigin only because CloudFront requires a target origin; (2) newsFunction serves news only on /api/news* and /news* and 404s everything else, so it stays safe even if it is ever wired to a broad behavior again (amplify/functions/news/__tests__/news-path-gate.test.ts). News also has its own explicit /api/news*NewsOrigin behavior now, instead of relying on the default.
  • Rule: every new api-v3 viewer path needs its own CloudFront behavior. Add it to amplify/cloudfront-dist-multiPath.json (the intended map) and sync the live distribution with ./run-amplify-update-cloudfront-behaviors.sh --wait E35RWLCSFHM5UN (additive + idempotent — it never deletes a live behavior). A missing behavior is now a loud 404 instead of a plausible-looking 200. Legacy /api/ai/chat* stays — MobileApp-Kisum docs still point at it.
  • All behaviors use origin-request policy f5a3e3d6-… (AllViewerExceptHostHeader) — forwarding the viewer Host breaks Lambda Function URL origins. cloudfront-dist-multiPath.json had drifted to AllViewer; applying that file wholesale would have broken every origin.
  • amplify_outputs.json is generated at deploy time and gitignored — production URLs are pinned in Console env vars unless you wire the build to read outputs.

Local dev

  • npx ampx sandbox from repo root deploys a personal sandbox stack and writes amplify_outputs.json.

This app is the current consumer of the Promoters BFF route split. Docs must stop describing promoter FE work as only living inside Frontend-Kisum.

The company profile includes a Users & Access area for shared teams, users, individual restrictions, Promoters-managed permissions, Finance access assignment, and Core business units. The old rollout-mode selector and preflight status were retired after all current companies moved to enforced access. It calls the Promoters BFF only. Frontend visibility is advisory; backend authorization remains authoritative.

Users & Access is the canonical Promoters-side editor for invitations, shared access, BU assignment, and Promoters-managed module/general permissions. Creating, renaming, and archiving business units lives on the sibling Business Units tab; Users & Access only assigns users to units that already exist. It does not manage artist.* or venue.* persona permissions. Finance access starts as Submitter here; Finance roles, limits, and primary approvers are managed inside Finance. Event and Tour forms require at least one BU and support direct user assignment.

The marketplace offer wizard follows the same rule when create event on submit is selected: each show defaults to the Core talent-programming system BU, displays the other active BUs for optional multi-selection, preserves the selection in drafts, and sends it with the offersheet. Existing-event links keep the linked event’s scope. Submit failures show a persistent error banner above the wizard footer (not only a toast). Step validation still blocks Next until required fields on that step pass.

The same form serves official and secondary listings, but every target retains its own offerChannel. Official availability submits as exclusive; secondary inventory submits as middle_agent. The browser calls the neutral Promoters BFF /api/artists-network/marketplace/offers, never the legacy Middle Agent enquiry path for an exclusive listing.

The combined Offers inbox preserves that distinction. Marketplace rows and post-submit redirects include source=marketplace; official records render Exclusive, Official agency, and the official representation notice. Secondary records alone render the Middle Agent badge and risk disclaimer.

Offer detail (/booking/offers/:id, 2026-07-15, PDF expanded 2026-07-17): mirrors the Artists agency inbox offersheet layout (OffersheetSummary — fee block with commercial deals + payment schedule, multi-show table or single-show date/venue/engagement, ticket scaling with totals footer, travel & hospitality, key details, cancellation ladder, clauses, buyer signatory, terms acceptance). Keeps the Official agency offer disclaimer banner at the top (hidden for offline offers). Promoter-only actions: Edit offer (purple, header position matching Artists Accept) and Download PDF (client-side @react-pdf/renderer; PDF exports the same offersheet sections as the detail view, plus appended Terms & Conditions from src/components/agents/offer-detail-pdf/tc.md via a lightweight markdown renderer — no page header, disclaimer, or duplicate on-screen performance-offer terms block). Offline offers: parties block shows buyer + artist only (no middle-agent seller row).

Because Exclusive offers are stored in normal Artists booking_offers, the inbox reads currentCommercialTermsJson.offerChannel before classifying a normal booking row. This prevents an Exclusive record from becoming “Direct” after submission or countering. Middle Agent rows continue to come from the separate enquiries query.

Artist photo/name in the inbox (fixed 2026-07-21). mapBookingRequestToOffer (src/services/apis/artists-network/mappers.ts) serves two differently-shaped sources: legacy /booking-requests rows, which are flat (artistName, artistImageUrl), and Artists v2 /booking-offers rows, which nest artist: { id, name, imageUrl, countryId, countryName }. It read only the flat keys, so every direct offer fell back to the literal name "Artist", an empty photo, and a lost artist id. The mapper now reads the nested artist block first and keeps the flat keys as fallback. Rule: this mapper must stay tolerant of both shapes — do not “clean it up” to one. (Artists also had to start returning imageUrl on the list rows; the two changes ship together.)

Offer wizard — payment merged into Fee & payment (2026-07-16)

Section titled “Offer wizard — payment merged into Fee & payment (2026-07-16)”

The wizard is 9 steps: the standalone “Payment schedule” step was removed. Payment lives inside Fee & payment as one choice — 100% before on-sale / 50%-50% / Custom. Presets write the offer’s payment wording only; Custom requires the structured installment plan (labels, amounts, due dates — this is what auto-creates the Finance invoice on accept) and the payment wording is auto-generated from those installments, so the legal text and the invoice plan can never contradict each other. offersheet.paymentPlan is emitted only for Custom; the offersheet contract consumed by Artists is unchanged.

Offer wizard — 11 steps, per-show engagement/billing, Cancellation & Ticket scaling steps (2026-07-17)

Section titled “Offer wizard — 11 steps, per-show engagement/billing, Cancellation & Ticket scaling steps (2026-07-17)”

Part of the platform’s contract data collection rollout (spec: modules/docs/superpowers/specs/2026-07-17-contract-data-collection-design.md; Phase A shipped in Artists on 2026-07-17). The wizard first grew a single “Deal terms” step (Phase B, same day); that step was deleted the same day and its contents redistributed into the current 11-step shape below — this section describes the final, shipped state.

The wizard is now 11 steps:

  1. Shows & dates — carries per-show engagement fields: engagementType (concert/club/festival/private), agePolicy, doors, curfew, billing (headline/co_headline/support/other). Stage time is not a new field — it reuses each show’s existing time.
  2. Fee & payment — gains sponsors switch, merchandising artist %/promoter % auto-complementing inputs + optional notes (switch-revealed), sales-report switch, hotel (payer/category/nights), local transportation, catering & hospitality, per diem (payer + amount per person/day).
  3. Cancellation & Force Majeure — reschedule period (default 12 months), reschedule costs, an editable 4-band cancellation ladder; these are the terms the agency must accept (see mutual-terms acceptance, Phase C, on the Artists side).
  4. Ticket scalingper show (2026-07-24): one block per show (date/venue), each with rows of name / price / capacity / comps and its own “Add ticket”. A multi-show offer can price each night differently.
  5. Key details — gains international/domestic travel (mode + buyout amount), travel class, excess baggage. 6-10. Clause steps (unchanged, shifted).
  6. Review & submit — mandatory buyer signatory block: name (mandatory), company and email prefilled from the org/user and editable.

The offersheet carries the same data as five new/changed sections: buyerSignatory (unchanged); engagement is REMOVED as a top-level section — those fields live on each shows[i] instead; commercialDeals reshaped to sponsors: boolean, salesReport: boolean, merchandising: {artistPct, promoterPct, notes?}; ticket scaling moved per-show (2026-07-24) — each shows[i].ticketScaling: Array<{name, price, capacity, comps}> (the old global commercialDeals.ticketScaling is kept read-only for legacy offers); travelHospitality unchanged; mutualTerms no longer carries billing (moved per-show). Artists passes all of it through to commercial_terms_json unchanged, and its contract generator reads the per-show engagement/billing and ticket scaling fields from the booking’s own show row. The Artists offersheet summary (booking offer detail) renders a per-show “Ticket sales” section.

Payment presets emit structured installments, not wording only: 100% before on-sale → one “Full payment” installment; 50-50 → a cent-safe Deposit/Balance split (remainder on Deposit); Custom is unchanged. Reopening a preset offer maps back to its stored preset instead of misreading it as Custom — fixes Schedule 2 payment rows staying empty on preset deals.

Promoters can save offline booking drafts in Promoters Postgres (promoters_db) without sending to the Artists marketplace pipeline.

EntryModeSubmit target
/booking/marketplace/enquire (no listingId)OfflinePOST /api/booking/offline-offers on Backend-Kisum-Promoters
/booking/marketplace/enquire?listingId=…Marketplace (unchanged)POST /api/artists-network/marketplace/offers
/booking/marketplace/enquire?artistId={numeric}Offline, artist lockedPromoters Postgres
Artist Team tab → Create offline offerOffline, artist lockedPromoters Postgres
  • Same 10-step enquiry wizard; step 0 only changes offline (artist search, optional listing link, global countries, optional promoter event via No event (skip)).
  • Offline artist search uses GET /api/artists/search on the Promoters BFF (machine directory proxy to Artists /api/v1/artists?q=), not GET /api/artists-network/artists — the network route is agency-scoped and requires an approved agency company claim, which promoters do not need for internal offline drafts.
  • Marketplace listing flow with ?listingId= is untouched.
  • Combined Offers inbox merges offline rows (badge Offline); detail tries marketplace first, then offline (/booking/offers/:id).
  • Permissions: promoter.booking.offers.create / promoter.booking.offers.view (same as marketplace create/view).
  • Data ownership: offline rows live in offline_offers (Prisma on Promoters BE); Artists marketplace offers remain Artists SoT.

Redirect-only routes go in proxy.ts, not in the page (2026-08-06)

Section titled “Redirect-only routes go in proxy.ts, not in the page (2026-08-06)”

/public/events is a landing page whose only job is to send the user to /dashboard. Doing that with redirect() inside the page produced a full-screen “Application error”.

redirect() throws to unwind the render. A 'use client' page cannot survive that: Next’s own App Router re-renders with a different hook count and dies with “Rendered more hooks than during the previous render”. Making the page a server component is not enough either — the route sits in the children parallel slot, so Next streams the redirect as an RSC payload (HTML 200 containing NEXT_REDIRECT) and the browser performs it, hitting the same fault.

Rule: a route that only redirects belongs in src/proxy.ts. A middleware redirect answers 307 before React renders anything, so the failure mode cannot occur. /public/events/[id] is a real page and is explicitly excluded.

Related: public/events/[id]/layout.tsx read eventData.data.title above its own if (!eventData?.data) guard, so a 403 threw a TypeError that the catch reported as a fetch failure. Read after the guard, never before.