Skip to content

Portal procurement: real pricing/trial/quote spine + linked-gated checkout (vertical slice)#6861

Draft
ConnorYoh wants to merge 20 commits into
mainfrom
portal-procurement-vslice
Draft

Portal procurement: real pricing/trial/quote spine + linked-gated checkout (vertical slice)#6861
ConnorYoh wants to merge 20 commits into
mainfrom
portal-procurement-vslice

Conversation

@ConnorYoh

Copy link
Copy Markdown
Member

Enterprise procurement — vertical slice (linked account → trial → quote → checkout)

Builds the real commercial spine on top of the merged UI shell (#6785): a linked team can start a (mock-licensed) trial, build a server-priced quote, and accept it to hand off to a Stripe checkout. Gated on a linked account — which also fixes a latent bug (the surface was gated on tier === "enterprise", unreachable in prod, so it never appeared).

Backend (app/saas)

  • ProcurementPricingService — faithful port of the marketing prototype's quotePricing (volume bands, SLA multiplier, term discount, add-ons, TCV). Unit-tested to the canonical quote (QT-AC9F-0001 = $41,400/yr, $124,200 TCV), volume bands, add-on stacking, seat→volume estimate.
  • ProcurementController /api/v1/procurement (@Profile("saas"), team resolved from the JWT principal, mutations leader-gated): snapshot, estimate, trial/start, trial/extend, quote (build), quote/{id}/accept.
  • ProcurementService deal lifecycle; trial issues a mock Keygen licence via the EnterpriseLicenseService seam (a no-card trial has no Stripe sub, so it's tracked on the deal, not billing_subscriptions).
  • Flyway V27__procurement.sqlprocurement_deal / quote / activity (stirling_pdf schema). Money in minor units (cents).

Frontend (frontend/portal)

  • Quote builder (QuoteBuilder.tsx) — server-priced, itemised.
  • Procurement view re-gated on useLink().isLinked: unlinked → link to begin; no deal → Start trial; then stepper + builder; accept → create-procurement-checkout edge function → redirect.
  • Marketing CTAs on Home ("Process millions of PDFs / Start Trial →") and Usage ("Standardize · 1M+ PDFs → Build your Enterprise quote"), both → /procurement; a direct /procurement load starts the journey (?start=quote opens the builder).
  • Real /api/v1/procurement client on apiClient.saas; i18n + CSS.

Companion change (separate repo)

The Supabase migration (twin of V27) + the create-procurement-checkout edge function live in Stirling-PDF-SaaS (branch procurement-vslice off v3) — a paired PR will follow there.

Verification

  • Backend: :saas compiles; pricing unit tests green.
  • Frontend: full gate green — typecheck, lint, prettier, build, build:portal, storybook, tests (94 editor + 16 portal test files).

How to test (real backend, not MSW)

Run the saas backend (ENABLE_SAAS=true), portal with mocks off + VITE_SAAS_API_URL, apply the SaaS migration on the v3 Supabase, deploy the edge function, and use a linked + team-leader account.

Draft — deliberate follow-ups (not in this PR)

  • Payment webhook → live: on checkout.session.completed, flip the deal to active, issue the annual licence, seed billing_subscriptions. Today checkout redirects out but nothing flips the deal to live yet.
  • No MSW mock for the new saas endpoints (real-backend-only in dev).
  • Agreement/e-sign stage skipped (accept → payment directly); trial-extend UI; CSS polish vs prototype.
  • Real Keygen management client (mock seam in place).

ConnorYoh added 3 commits July 1, 2026 16:58
Canonical pricing engine (ProcurementPricingService) ported from the marketing prototype's quotePricing, unit-tested to the QT-AC9F-0001 numbers ($41,400/yr, $124,200 TCV) plus volume bands, add-on stacking and the seat->volume estimate. Adds the Flyway V27 schema (procurement_deal/quote/activity) + JPA entities and repositories. Rates come from a PricingRates card (defaults() mirrors the prototype; Stripe-mirror-backed catalog to follow). No controller/checkout wired yet.
ProcurementController (/api/v1/procurement, @Profile saas, team resolved from principal, leader-gated mutations): snapshot, trial start/extend, build quote (server-priced), accept quote. ProcurementService orchestrates the deal lifecycle; trial issues a mock Keygen licence via the EnterpriseLicenseService seam (no Stripe — a no-card trial has no subscription). Accept advances to the payment stage where the portal hands off to a Supabase checkout edge function. Compiles clean.
Real /api/v1/procurement client (apiClient.saas): snapshot, trial start/extend, build/accept quote. QuoteBuilder (server-priced, itemised). Procurement view re-gated on a LINKED account (fixes the dead tier==='enterprise' gate): unlinked -> link-to-begin, no deal -> start trial, then stepper + builder + accept->checkout edge fn. Sidebar entry gated on linked. Home + Usage carry the marketing CTAs (Process millions of PDFs / Standardize -> Build your quote), both routing to /procurement; a direct /procurement load starts the journey. i18n + CSS added. Full frontend gate green (typecheck/lint/format/build/build:portal/tests/storybook). Backend snapshot returns a single shape (empty when unstarted).
@stirlingbot stirlingbot Bot added the Front End Issues or pull requests related to front-end development label Jul 2, 2026
ConnorYoh added 4 commits July 2, 2026 11:08
…sJpaConfig

The saas module scans JPA entities/repos from an explicit package list; add stirling.software.saas.procurement.{model,repository} so ProcurementDealRepository/ProcurementQuoteRepository wire at startup (fixes 'required a bean ... ProcurementDealRepository' on boot).
…ectable bean)

Boot failed wiring procurementService/procurementController — the saas application context exposes no ObjectMapper bean. Use a private static ObjectMapper for the line-items JSON (de)serialisation instead of constructor injection.
…book

Rebuilds QuoteBuilder faithfully from the marketing prototype: 4 steps (Volume / Commitment & service / Details / itemised Quote), radio + checkbox option cards WITH descriptions (Standard/Priority+15%/Dedicated+30%, add-ons), term pills + discount note, live footer running total, and the branded itemised quote paper — at the prototype's tight density (exact colours/spacing). Adds a procurementSaas MSW handler so trial/quote/accept work in Storybook + mocks-on dev, plus QuoteBuilder + EnterpriseUpsellCard stories. Initialises i18n in .storybook/preview (Suspense + side-effect import) so t()-copy resolves in stories (fixes the portal-wide raw-keys gap). Full frontend gate green.
…dd reset

Issue 1 from demo feedback: procurement is no longer a sidebar tab. It now lives
on Home as a compact deal-status hero (active deal) or the enterprise upsell
on-ramp (not started), both expanding into a full-screen takeover modal that
hosts the whole journey (link-gate -> start trial -> quote builder -> checkout),
matching the marketing prototype.

- DealStatusHero: compact Home banner (eyebrow, company, trial countdown, stage
  stepper, adaptive next-step CTA) that opens the modal.
- ProcurementModal: portaled full-screen takeover shell (blurred backdrop,
  Escape/backdrop close), copying the prototype modal design.
- ProcurementHome: orchestrator - fetches the snapshot, renders hero-or-upsell,
  hosts the modal flow, wires start-trial / accept-quote -> Stripe checkout.
- Home renders <ProcurementHome/>; /procurement route is a thin autoOpen wrapper
  for deep links; procurement nav entry removed from the Sidebar.
- Reset: ProcurementDealRepository.deleteByTeamId + ProcurementService.resetDeal
  + POST /api/v1/procurement/reset (leader-gated) + resetProcurement() client +
  MSW handler; surfaced as a demo "Reset procurement" link in the modal.
- i18n: [procurement.hero.*] + procurement.reset keys (en-US).
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Frontend Check Failed

There are issues with your frontend code that will need to be fixed before they can be merged in.

Run task frontend:fix to auto-fix what can be fixed automatically, then run task frontend:check:all to see what still needs fixing manually.

ConnorYoh added 12 commits July 2, 2026 13:34
Issue 2 from demo feedback: back/forth editing of a committed quote. The accepted
payment card now offers "Edit quote", which reopens the builder seeded from the
quote's own config; reviewing produces a fresh quote that supersedes the accepted
one (checkout prices inline per session, so there is no stale Stripe price to clean
up).

- Quote responses echo the priced config (QuoteConfigEcho) so the builder can seed
  itself; users is not persisted (only volume), so the seeded volume is treated as
  manually set.
- QuoteBuilder takes an optional `initial` config; ProcurementHome tracks an
  `editing` flag and renders the seeded builder, resetting it on accept/reset.
- MSW quote mock echoes config; i18n adds procurement.payment.edit (en-US).
The hero and takeover-modal panel used var(--color-bg-elevated), which is not a
defined design token, so it resolved to nothing and the surfaces rendered
transparent over the page/backdrop. Switch both to var(--color-surface) (the
real elevated-surface token). Add a ProcurementModal story so the opaque panel
over the dimmed backdrop is verifiable in Storybook.
…duplicate

The Usage/billing page already had an Enterprise upsell with a (disabled) "Build
your Enterprise quote" CTA embedded in the Spend-this-month card; my separate
EnterpriseUpsellCard on Usage duplicated it. Remove the duplicate and enable the
existing CTA, wiring it to the procurement flow via setActiveView("procurement")
(the /procurement route auto-opens the quote builder in the takeover modal).

- Usage.tsx: drop the EnterpriseUpsellCard row + import.
- billing/EnterpriseUpsell.tsx: enable the CTA, navigate to the procurement view.
- Delete the now-unused procurement/EnterpriseUpsellCard{.tsx,.stories.tsx} and its
  usage* i18n keys (home* keys stay — ProcurementHome's on-ramp uses them).
…accept)

Replace the inline-checkout model with Stripe Quotes: building a quote now produces a
priced DRAFT; "Generate quote" issues it as a finalized Stripe Quote (real number +
PDF, shareable), which becomes a durable milestone the buyer can download, leave, and
return to; accepting it has Stripe create the committed annual subscription + first
invoice. The Java pricing engine stays; Stripe lives in edge functions (SaaS repo).

Backend (:saas):
- ProcurementQuote: stripe_quote_id + stripe_invoice_url columns (Flyway V28).
- buildQuote persists DRAFT (the edge fn flips it to SENT on issue).
- QuoteResponse drops checkoutFunction, adds stripeQuoteId + invoiceUrl; the
  /quote/{id}/accept endpoint is retired (accept is now an edge function).

Frontend:
- api: issueQuote / acceptQuote / fetchQuotePdf invoke the edge functions; QuoteResult
  gains status/stripeQuoteId/invoiceUrl.
- QuoteBuilder terminal action is "Generate quote" (onGenerate) not accept.
- ProcurementHome: draft → builder; issued (sent) → milestone card (Download PDF /
  Accept & subscribe / Edit); accepted → "Subscription created" + View invoice. Returns
  to the milestone on reload instead of dumping into a fresh builder.
- MSW mocks the three edge functions; i18n adds procurement.milestone.* +
  payment.viewInvoice + builder.generate (drops the retired keys). Added a
  ProcurementHome story; verified the full flow in Storybook.
…, drop Acme placeholder

Addresses demo feedback:
- Agreement is now a real stage. Milestone "Accept & continue" advances the deal to
  the security/agreement stage (new Java POST /agreement); a ProcurementAgreement view
  shows the combined Stirling Enterprise Agreement (MSA + Order Form from the quote +
  EULA + DPA) with an "I agree" checkbox + "Agree & subscribe" (no e-signature yet),
  which then accepts the quote into the subscription.
- Starting a trial is one click: the Home "Start Trial" CTA (and a /procurement deep
  link) start the trial directly instead of opening a modal that asks again; the hero
  then shows the trial deadline and next steps. Removed the redundant in-modal start
  screen (and its procurement.start.* copy).
- Hero gains an "Extend trial" action during the trial (wired to extendTrial), alongside
  the adaptive next-step CTA; added ctaAgreement for the security stage.
- Removed the "Acme Corp" placeholder from the quote builder (now a generic
  "Your company"). Note: unrelated demo fixtures elsewhere still use Acme/Globex-style
  fictional names — left out of this change.
- MSW: /trial/extend + /agreement handlers; i18n adds procurement.agreement.* +
  hero.ctaAgreement/extendTrial + builder.businessNamePlaceholder. Verified the full
  flow (auto-start → build → generate → milestone → agreement → subscription) in Storybook.
…d review

- Milestone card now shows the full line-item breakdown (matching the review page), not
  just the annual + TCV totals.
- Editing a quote seeds the builder from the existing config (it already did) and now
  jumps straight to the filled review step instead of restarting at step 1, so nothing
  looks reset — the buyer can Back to change a field or re-generate.
- Payment card: clearer copy — pay or download the invoice right here (no email); the
  invoice is finalized immediately server-side so the hosted pay link is available now.
…ecklist, side dialogs

Bring the deal-status hero up to the marketing prototype:
- Quick-action chips next to the deal: trial-countdown (opens trial management), Key
  documents, Invite teammates, Schedule a call — shown per stage (docs/invite hidden
  once live).
- Rollout checklist during the trial (Deploy the PDF Editor / Connect the PDF Processor
  / Add recommended policies) with "Not started" pills, each navigating to the relevant
  view.
- Adaptive primary CTA copy per stage (Build your quote → Review your quote → Review &
  sign agreement → Add payment).
- New side dialogs (ProcurementExtras): Key documents (deal + supporting ledger),
  Schedule a call (solutions engineer + time slots), and trial management (extend /
  cancel). Content is mocked for the pilot; shells + wiring are real. Invite teammates
  navigates to the Users view.
- i18n: procurement.hero.{inviteTeammates,scheduleCall,notStarted,setup1..3*} +
  keyDocs relabelled "Key documents". CSS for chips/checklist/side-modals/docs/slots.

Verified in Storybook (enriched trial hero + Key documents modal). typecheck + 109
tests + storybook build green.
Fetching the Stripe quote PDF goes through an edge function and takes a moment; the
"Download PDF" button now shows its loading spinner while the fetch is in flight so it
doesn't feel unresponsive.
…mise agreement

- Download PDF: use a same-gesture <a download> click instead of window.open (which was
  popup-blocked after the async fetch — the "few goes" problem), and surface a friendly
  message if the fetch fails. The edge fn also retries while Stripe renders the PDF.
- Remember the company name: businessName is now part of the quote config — persisted
  (ProcurementQuote.business_name, Flyway V29 + Supabase twin), echoed back, and folded
  into the builder's cfg so re-editing shows "Prepared for <company>" again.
- Agreement Order Form now lists the itemised breakdown (volume, service level, add-ons,
  discount), matching the quote/review — not just the term + totals.
…a11y, price guard

- One fewer step to a quote: the builder is now 3 steps (volume / commitment / details);
  completing the form "Generate quote" builds + issues directly, landing on the milestone
  (which shows the quote). Removed the redundant in-builder quote-paper preview + its
  unused i18n keys. Editing jumps to the details step with the agreement pre-accepted.
  Milestone now shows "Prepared for <company>".
- Payment step: add a "Download invoice" action (from the accept response's invoice PDF)
  alongside "View & pay invoice".
- Deep link: /procurement no longer silently starts a trial — with no deal it shows the
  Start-trial CTA; it only opens the flow modal once a deal is underway.
- a11y: focus-trap (useFocusTrap) on the takeover modal and side dialogs (focus on open,
  Tab wraps, focus restored on close).
- Pricing: clamp volume to non-negative server-side (defence-in-depth; the rate card +
  formula are server-side so the browser can't lower a price for a given config).
…urney

- Error surfacing: run() now catches failures from start-trial / issue / accept / extend /
  go-live and shows a dismissable danger Banner in the modal; the PDF download error routes
  there too (no more silent spinner-stops or window.alert).
- Mock go-live: a demo/manual stand-in for the deferred invoice.paid webhook — Java
  ProcurementService.markLive (issues the annual licence via the EnterpriseLicenseService
  seam, advances to active) + POST /procurement/go-live (leader-gated) + goLive() client +
  MSW handler. Surfaced as a "Simulate payment received (demo)" link on the payment card,
  and the deal then shows a "You're live on Stirling Enterprise" state so the full
  trial → quote → agreement → payment → live journey completes without the webhook.
- i18n: procurement.error.title, procurement.payment.simulate, procurement.live.*.
…lice

# Conflicts:
#	frontend/.storybook/preview.tsx
#	frontend/editor/src/portal/components/billing/EnterpriseUpsell.tsx
#	frontend/editor/src/portal/components/procurement/DealStatusHero.stories.tsx
#	frontend/editor/src/portal/components/procurement/DealStatusHero.tsx
#	frontend/editor/src/portal/components/procurement/ProcurementAgreement.tsx
#	frontend/editor/src/portal/components/procurement/ProcurementExtras.tsx
#	frontend/editor/src/portal/components/procurement/ProcurementHome.stories.tsx
#	frontend/editor/src/portal/components/procurement/ProcurementHome.tsx
#	frontend/editor/src/portal/components/procurement/ProcurementModal.stories.tsx
#	frontend/editor/src/portal/components/procurement/ProcurementModal.tsx
#	frontend/editor/src/portal/components/procurement/QuoteBuilder.stories.tsx
#	frontend/editor/src/portal/components/procurement/QuoteBuilder.tsx
#	frontend/editor/src/portal/mocks/handlers/procurementSaas.ts
#	frontend/editor/src/portal/views/Procurement.tsx
#	frontend/portal/public/locales/en-US/translation.toml
Self-review findings #2/#3: /go-live (mock invoice.paid stand-in — issues the licence and
goes active without payment) and /reset (deletes the deal) shipped in prod code, leader-gated
only. Gate both behind stirling.procurement.demo-controls-enabled (default false) so they
return 404 unless explicitly enabled in a demo/dev environment. Enable with
STIRLING_PROCUREMENT_DEMO_CONTROLS_ENABLED=true for demos; the MSW-mocked flow is unaffected.
@stirlingbot

stirlingbot Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🚀 V2 Auto-Deployment Complete!

Your V2 PR with embedded architecture has been deployed!

🔗 Direct Test URL (non-SSL) http://54.175.155.236:6861

🔐 Secure HTTPS URL: https://6861.ssl.stirlingpdf.cloud

This deployment will be automatically cleaned up when the PR is closed.

🔄 Auto-deployed for approved V2 contributors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Front End Issues or pull requests related to front-end development Translation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant