Frontend Promoters
Users & Access
Section titled “Users & Access”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.viewremoves 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.
Company profile
Section titled “Company profile”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 UI (2026-07-23)
Section titled “Messages UI (2026-07-23)”/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.
Event identity in the UI (2026-08-01)
Section titled “Event identity in the UI (2026-08-01)”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.
Events / Tours tab counts (2026-08-01)
Section titled “Events / Tours tab counts (2026-08-01)”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.
Dashboard pipeline block (2026-07-23)
Section titled “Dashboard pipeline block (2026-07-23)”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).
Runtime status
Section titled “Runtime status”Frontend-Kisum-Promoters is implemented.
The repo README is not the current contract. Use the repo TODO/changelog and the live route tree instead.
Purpose
Section titled “Purpose”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.
Wrong-app access guard
Section titled “Wrong-app access guard”Promoters is only for the promoter persona. Artist and venue users must be sent to their own app.
| Layer | Behavior |
|---|---|
Client (app-shell-layout) | Derived from the active company’s access payload that GET /api/users/init already returns (useInitializeUser → result.wrongApp). No browser → Auth call. |
| Block UI | WrongPackageScreen — link target is Auth packageAppUrl (Auth owns persona → URL map), carried through init’s access.packageAppUrl. |
| Allow | Auth package: promoter, promoter module keys, and Core package types such as basic_promoter. |
| Block | Known artist/agency and venue personas only — not empty/unresolved package (fail-open). |
| No active company | No verdict — you cannot be in the wrong app before picking a company; the company picker shows instead. |
| Root layout | No server-side package guard — avoids racing the x-org cookie and keeps auth/init visible in browser DevTools (same pattern as Artists). |
Implemented now
Section titled “Implemented now”Promoter-specific booking network
Section titled “Promoter-specific booking network”- 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 requirepromoter.booking.agencies.viewfrom Auth and must not appear in Coreaccess.deniedPermissionKeys(restriction packages). Owner/admin bypass does not apply when the org has an active restriction (e.g. seededagencies_contact). Global state:deniedPermissionKeysfrom/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
Venue sourcing
Section titled “Venue sourcing”Venues (Postgres SoT only)
Section titled “Venues (Postgres SoT only)”/venues— All Venues directory reads onlyBackend-Kisum-VenuesPostgreSQL via Promoters BFFGET /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,SGonGET /api/venues; Venues admin list filters server-side (repeatediso2=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).
Billing and profile
Section titled “Billing and profile”/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
Existing promoter CRM still present
Section titled “Existing promoter CRM still present”- dashboard
- vendors
- workspace/tasks
- AI/research
- public event pages
- market analysis / rankings / festivals
Add / Edit Event form (2026-07-31)
Section titled “Add / Edit Event form (2026-07-31)”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).
| Surface | Route family | BFF 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-workspace | same (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/).
Current route model
Section titled “Current route model”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 preferslugviaartistDetailPath(from PromotersGET /api/artistsBFF)/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=marketplaceso the page reads the neutral marketplace workflow directly. Detail copy follows the storedexclusive|middle_agentchannel./booking/confirmed— listsstatus=confirmedper-show bookings from ArtistsGET /bookings(promoter buyer org scoped viabuyer_core_company_idafter 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]onFullstack-Kisum-Public
Pending / not fully closed
Section titled “Pending / not fully closed”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).
| Phase | What deploys | How |
|---|---|---|
| Backend | All amplify/ CDK resources | amplify.yml → npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID on each connected-branch build |
| Frontend | Next.js SSR app | Same amplify.yml → pnpm run build:production |
Requirements
- Amplify Console app connected to this repo/branch (prod app ID
d1h8upn1gdp1fp, regionap-southeast-1). - App service role with
AmplifyBackendDeployFullAccess(required foramplify.ymlbackend phase). - One-time
cdk bootstrapinap-southeast-1per AWS account. - Env contract:
amplify-variables.json/ Amplify Console (NEXT_PUBLIC_FUNCTION_URL_*, auth, BFF URLs). - CloudFront at
api-v3.kisum.dev(distributionE35RWLCSFHM5UN) is manual — not updated bypipeline-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.tsmust setbundling.depsLockFilePath: 'pnpm-lock.yaml'(repo is pnpm-only; do not point at missingpackage-lock.json). Missing this onrouterFunctioncaused Amplify CIFailed to build asset function/routerFunction-lambda/Code. - No Amplify Data:
defineBackenddoes not includedata. StarterTodo/ AI conversation AppSync routes were unused and caused CICDKAssetPublishErroron 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 CloudFormationCDKToolkitstill lists it, CI fails withCDKAssetPublishError/AggregateErroron random assets — recreate the bucket (job 551 recovered this way on 2026-07-10).
Canonical ops runbook: repo amplify/README.md → Production 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 injectsX-Origin-Keyat the edge. Server routes also sendx-auth,x-org, andx-origin-keyviasrc/libs/api-v3-proxy.ts. longOpsFetchusesapi-v3path/api/ai/longops/*with JWT +x-org;aiLongOpsstrips that prefix before proxying to Promoters BFF.newsFunctioncalls MusicData with machine auth only (not the viewer JWT). Phase 2 deploy 1: whenS2S_TOKENS_ENABLED=true, mints Auth service token aspromoters→ audiencemusicdata(X-Service-Token); otherwise falls back to staticMUSICDATA_INTERNAL_API_KEY. Console secrets:AUTH_SERVICE_SECRET(same as Promoters BE) + optional static key; never commit secrets inamplify/lambda-env.ts.artistDemographic,artistPredictionv3,artistTrends, andaiLongOpsverify Kisum JWT +x-orgat the Lambda edge (Auth JWKS) before calling Promoters BFF; env:AUTH_JWT_VERIFICATION,AUTH_JWT_ISSUER,AUTH_JWT_AUDIENCE(seeamplify-variables.json).aiChatmaps viewer paths/ai/chat→ upstreamPOST /chat,/ai/sessions→GET|DELETE /sessionsonAI_CHAT_UPSTREAM_URL. Lambda converts viewerx-authto upstreamAuthorization: Bearer ….- Production KisumAI path: Next
/api/ai/*→NEXT_PUBLIC_FUNCTION_URL_ROUTER(https://api-v3.kisum.dev) → router →aiChatLambda →AI_CHAT_UPSTREAM_URL. Local dev still bypasses api-v3 and callsAI_CHAT_UPSTREAM_URLdirectly (NODE_ENV=development). Do not setAI_CHAT_UPSTREAM_URLtoapi-v3.kisum.dev— that is the CloudFront proxy, not the AI backend. - The CloudFront default behavior (
*) fails closed. It used to targetNewsOrigin, so a path with no matching behavior returned the MusicData news payload with HTTP 200 instead of an error. On 2026-07-28 productionGET /ai/sessionsandPOST /ai/chathad no behavior and silently returned{ "excerpt": [...] }; the Promoters BFF surfaced it as502 Chat history upstream returned news data instead of sessions. Dev was unaffected becauseNODE_ENV=developmentbypasses api-v3 and callsAI_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, acloudfront-js-2.0viewer-request function on the default behavior, returns404 {"error":"Not Found"}without reaching an origin — the default still namesNewsOriginonly because CloudFront requires a target origin; (2)newsFunctionserves 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*→NewsOriginbehavior 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-Kisumdocs still point at it. - All behaviors use origin-request policy
f5a3e3d6-…(AllViewerExceptHostHeader) — forwarding the viewerHostbreaks Lambda Function URL origins.cloudfront-dist-multiPath.jsonhad drifted toAllViewer; applying that file wholesale would have broken every origin. amplify_outputs.jsonis 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 sandboxfrom repo root deploys a personal sandbox stack and writesamplify_outputs.json.
Important rule
Section titled “Important rule”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.
Users and Access
Section titled “Users and Access”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.
Permissions V2 management UI
Section titled “Permissions V2 management UI”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:
- 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 existingtime. - 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).
- 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). - Ticket scaling — per show (2026-07-24): one block per show (date/venue), each with rows of
name / price / capacity / compsand its own “Add ticket”. A multi-show offer can price each night differently. - Key details — gains international/domestic travel (mode + buyout amount), travel class, excess baggage. 6-10. Clause steps (unchanged, shifted).
- 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.
Offline promoter offers (2026-07-17)
Section titled “Offline promoter offers (2026-07-17)”Promoters can save offline booking drafts in Promoters Postgres (promoters_db) without sending to the Artists marketplace pipeline.
| Entry | Mode | Submit target |
|---|---|---|
/booking/marketplace/enquire (no listingId) | Offline | POST /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 locked | Promoters Postgres |
| Artist Team tab → Create offline offer | Offline, artist locked | Promoters 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/searchon the Promoters BFF (machine directory proxy to Artists/api/v1/artists?q=), notGET /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.