mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Consolidates the enterprise procurement and legal work into one PR off `main`. Supersedes #7020 (closed; every commit from it is contained here). Sits on top of PAYG prepaid bundles (#7032) and the `--color-*` → `--c-*` portal token rename. ## Why Enterprise procurement was a mock. The stage screens read from a fake state machine, the "agreement" was prose hardcoded in a component, and nothing a buyer did was recorded anywhere. To actually sell to an enterprise we need three things it didn't have: a real document they can read and sign, a record that proves they signed that exact version, and a licence that flips when they pay. ## What **The agreement is a real versioned document** - Registry at `resources/legal/manifest.json` + `legal/<id>/<version>/*.md`. Publishing a new version is a markdown file and a manifest bump, no code change. `@`-prefixed parts are generated sections. - `AgreementAssembler` builds MSA (Part A) + generated Order Form (Part B) + DPA (Part C) as one document. Only the Order Form varies per deal. - `AgreementPdfRenderer` goes through our own pipeline (commonmark → `FileToPdf`/WeasyPrint), so we dogfood it. - Immutable signature record pinning document id and version, a SHA-256 of the exact rendered markdown, the variable snapshot, typed signatory details, timestamp and IP. **Legal document pages and consent logging** - `GET /api/v1/legal/{docId}` serves any registry document; a viewer modal renders it with a draft badge. The SLA exhibit is viewable for the first time. - `legal_consent` + `POST /api/v1/legal/consent`. EULA clickwrap is recorded once: at trial start, or at the quote step only if there was no trial. **Quote and Agreement are separate steps** The quote step is a plain itemised review (figures, renewal, PO) with download and "Accept quote". Accepting advances to the agreement and does not charge Stripe. Signing the agreement is still the commitment point. **One quote number** We no longer mint our own reference. The Stripe quote number is the identifier everywhere, so the UI and the memo can't disagree. `quote_number` is nullable until Stripe assigns it at finalisation (`20260808000000`). **Payment takes the deal live** `invoice.paid` on the stripe-webhook moves the deal to live and the UI reflects it. Nothing watched for payment before, so a paid customer sat in "payment" forever. Needs `invoice.paid` enabled on the webhook endpoint in the Stripe dashboard. **Security** Any signup could self-issue a $0 enterprise licence, from three things compounding: leader-on-signup, no entitlement gate, and no ACV floor. So: `startTrial` now has a stage guard (it was replacing committed licences), the offline `.lic` is gated on entitlement, the ACV floor is enforced before the quote persists, and the air-gap check reads the quote's deployment rather than the deal's. Invitee emails are redacted in logs. Dev and Storybook were hitting real Stripe; both now route through `resolveDemoResponse`. **Removed the dead procurement island** The original stage-by-stage page survived the rebuild with no route and no consumer, so it was invisible to review but still cost a reader's time. 16 unreferenced files, 182 lines of superseded API, 53 orphaned en-US keys, and `Procurement.css` from 1665 to 968 lines. Nothing deleted had a live consumer. ## Screenshots Home, deal underway (hero card footer): <!-- home-in-procurement.png --> Quote builder, step 1: <!-- quote-builder.png --> Agreement, ready to sign: <!-- agreement-signing.png --> Payment and live: <!-- stage-payment.png / stage-live.png --> ## How to test **Storybook** covers every state without a backend: ```bash cd frontend && npm run storybook ``` Then `Portal/Procurement/*`: | Story | What to look at | | --- | --- | | `DealStatusHero` — Trial / Quote / Agreement / Payment / Live | One hero per stage: progress band, one-line status, stage CTA | | `QuoteBuilder` — Default | 4 steps. Users + volume drive the price; Governance and PDF size are multipliers; step 4 is the itemised review | | `ProcurementAgreement` — Default / Signing | Header actions, always-visible scrollbar on the paper, one-line signature row | | `ProcurementStages` — Payment / Live / License | "View & pay invoice" opens Stripe directly; licence key and `.lic` download | | `Views/Home` — Subscribed In Procurement | The hero in real page context | Note: `ProcurementAgreement` renders "Could not load the agreement" in Storybook because it fetches the document from the backend. The chrome is accurate, the paper body needs the app. **Full flow** needs SaaS running and a linked team: 1. Home → **Explore enterprise** → trial setup (deployment + seats). EULA is recorded here. 2. **Build your quote** → 4 steps → Generate. Buyer details are required first. 3. Review the itemised quote → **Accept quote**. Confirm Stripe was *not* charged. 4. Agreement → tick, fill signatory, **Sign agreement**. Check `procurement_signature` for the version and content hash. 5. **View & pay invoice** → pay in Stripe test mode → deal should move to live on the `invoice.paid` webhook. Worth reviewing specifically: the licence cannot be issued without entitlement (step 3 before payment), and `startTrial` on an already-committed deal is rejected rather than overwriting. ## Verification - `:saas compileJava` + `spotlessJavaCheck` - `task frontend:check:all` green end to end: 9 typecheck variants, eslint at zero warnings, `theme-lint`, `lint:css`, prettier, build, **1656 tests across 188 files** - 7 deno tests on the `invoice.paid` handler, covering all four shapes Stripe uses for the subscription reference ## Open, not addressed here - **The commercial model contradicts itself in three places.** The Order Form says annual-in-advance, the MSA §2.3/§3.2 implies otherwise, the quote engine computes `tcv = annualNet × termYears` flat, and Stripe only invoices one year. Needs a decision before this is customer-facing. - The 25 MB data-processing increments vs the ×1.4/×2.4 size multiplier, deferred pending Matt. - All legal text is **draft**. It renders with a draft badge and is not presented as executed; counsel's read is still a publish gate. - `{{subprocessor_url}}` / `{{eula_url}}` awaiting marketing's final links. - `frontend-a11y` is red on pre-existing portal contrast debt, deferred by decision. ## Schema notes Two migrations land on the SaaS side (`v3`), both applied by that repo's PR CI: - `20260808000000` drops the NOT NULL on `procurement_quote.quote_number`, which is required rather than cosmetic — the number now comes from Stripe at finalisation, so a draft holds NULL, and `ddl-auto` cannot drop an existing NOT NULL itself. - `20260809000000` adds `procurement_deal.last_paid_invoice_id`, nullable. Nothing here needs a migration in this repo: Flyway is not on the classpath, so the Java side only ever adds via `ddl-auto`, and Postgres migrations run ahead of the app deploy.
115 lines
2.6 KiB
CSS
115 lines
2.6 KiB
CSS
/* ──────────────────────────────────────────────────────────────────────── */
|
|
/* Subscribed-tier deployed-Editor hero */
|
|
/* ──────────────────────────────────────────────────────────────────────── */
|
|
|
|
.portal-editor-hero {
|
|
border-radius: var(--radius-xl);
|
|
border: 1px solid var(--c-border);
|
|
background: var(--c-surface);
|
|
box-shadow: var(--shadow-sm);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.portal-editor-hero__row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.625rem 0.875rem;
|
|
background: var(--c-surface);
|
|
}
|
|
|
|
.portal-editor-hero__logo {
|
|
width: 2rem;
|
|
height: 2rem;
|
|
border-radius: 0.5rem;
|
|
overflow: hidden;
|
|
flex-shrink: 0;
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
.portal-editor-hero__mark {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: block;
|
|
}
|
|
|
|
.portal-editor-hero__info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.portal-editor-hero__title-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5625rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.portal-editor-hero__name {
|
|
font-size: 0.9375rem;
|
|
font-weight: 700;
|
|
color: var(--c-text);
|
|
margin-right: 0.125rem;
|
|
}
|
|
|
|
/* The deployment host, sat inline with the name as the rail's subject. */
|
|
.portal-editor-hero__host {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.8125rem;
|
|
font-weight: 500;
|
|
color: var(--c-text-muted);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Quiet separator between the host and the adoption count. */
|
|
.portal-editor-hero__dot {
|
|
width: 0.3125rem;
|
|
height: 0.3125rem;
|
|
border-radius: 999px;
|
|
background: var(--c-border);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.portal-editor-hero__actives {
|
|
font-size: 0.75rem;
|
|
color: var(--c-text-muted);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.portal-editor-hero__meta {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
flex-wrap: wrap;
|
|
font-size: 0.75rem;
|
|
color: var(--c-text-subtle);
|
|
}
|
|
.portal-editor-hero__meta-sep {
|
|
color: var(--c-border);
|
|
}
|
|
|
|
.portal-editor-hero__action {
|
|
flex-shrink: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
@media (max-width: 48rem) {
|
|
.portal-editor-hero__row {
|
|
flex-wrap: wrap;
|
|
}
|
|
.portal-editor-hero__action {
|
|
width: 100%;
|
|
}
|
|
}
|
|
|
|
/* Attached footer strip (setup checklist). */
|
|
.portal-editor-hero__footer {
|
|
border-top: 1px solid var(--c-border-subtle);
|
|
}
|