Stripe Checkout, receipts and the card statement carry the Stripe account's public business name and statement descriptor, which Stripe fills from the registered legal name until they are set. Where that name differs from the deployment's, a member first meets it above the card fields (#13). Each install has to notice this on its own, since a fix in one account's settings reaches no other deployment. - A Temporal workflow reads the public details at every boot and on request, with whatever permission the key holds: the account itself with Accounts Read, otherwise an invoice preview and the latest charge. The result is a JSON instance setting; the account id is never stored. - The Stripe provider page gains a Public details section. Its Read again control, and the Environment check's Check now, move into the section header as a refresh action. - The Products page and the checkout return name who bills the member and what the card statement says, only when a name would not be recognised against deployment-name. Every control that costs money, the switch preview's commit included, references the line through FormView.CommitDescribedBy. - docs/stripe.md gains "What members see"; production-deployment.md points to it. golang.org/x/text becomes a direct dependency for the name comparison. OpenSpec change merchant-disclosure is archived and its six deltas synced into openspec/specs/.
124 lines
4.5 KiB
Go
124 lines
4.5 KiB
Go
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
|
|
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
|
|
|
|
package server
|
|
|
|
import (
|
|
"fmt"
|
|
"html/template"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
)
|
|
|
|
// Server-side composition (spec page-anatomy "A page arrives complete";
|
|
// docs/design-system.md §6). A page region that also exists as an HTMX
|
|
// partial, so a mutation can swap it back in, is rendered into the page by
|
|
// the server on first paint through an in-process request to the partial's
|
|
// own route. The browser never fetches first-paint content after load: one
|
|
// response, no spinner, and the partial stays the single source of the
|
|
// region's markup.
|
|
|
|
// Include renders a GET route in-process, with the caller's request context
|
|
// (session, request ID) and headers, and returns its HTML fragment. Core
|
|
// builds one against its router and hands it to integrations through Deps
|
|
// so their card bodies compose the same way.
|
|
type Include func(r *http.Request, path string) (template.HTML, error)
|
|
|
|
// NewInclude returns an Include that dispatches through router. The router
|
|
// is the mux the partial routes are registered on; the middleware stack sits
|
|
// outside it, so an include never re-authenticates: the session already
|
|
// rides the context it is given.
|
|
func NewInclude(router http.Handler) Include {
|
|
return func(r *http.Request, path string) (template.HTML, error) {
|
|
u, err := url.Parse(path)
|
|
if err != nil {
|
|
return "", fmt.Errorf("include %s: %w", path, err)
|
|
}
|
|
req := r.Clone(r.Context())
|
|
req.Method = http.MethodGet
|
|
req.URL = u
|
|
req.RequestURI = path
|
|
req.Body = http.NoBody
|
|
req.ContentLength = 0
|
|
req.Header.Set("HX-Request", "true")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
return "", fmt.Errorf("include %s: status %d", path, rec.Code)
|
|
}
|
|
return template.HTML(rec.Body.String()), nil
|
|
}
|
|
}
|
|
|
|
// IncludeOr renders the fragment, or an inline notice naming what could not
|
|
// be loaded, so a failing region never fails the page and never renders
|
|
// blank. A nil Include (a test set without a router) renders nothing.
|
|
func IncludeOr(inc Include, logger *slog.Logger, r *http.Request, path, what string) template.HTML {
|
|
if inc == nil {
|
|
return ""
|
|
}
|
|
out, err := inc(r, path)
|
|
if err != nil {
|
|
if logger != nil {
|
|
logger.Error("page region failed to compose", slog.String("path", path), slog.Any("error", err))
|
|
}
|
|
return template.HTML(`<div class="alert alert-danger" role="alert">Could not load ` + template.HTMLEscapeString(what) + `.</div>`)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// DashboardCardView is a declared card with its body composed for this
|
|
// request. PartialPath stays the swap source for the card's RefreshEvent.
|
|
type DashboardCardView struct {
|
|
DashboardCard
|
|
Body template.HTML
|
|
}
|
|
|
|
// ProductsPageData is the /products page: the shell plus its three regions,
|
|
// composed from their partial routes.
|
|
type ProductsPageData struct {
|
|
Shell Shell
|
|
Entitlements template.HTML
|
|
Plans template.HTML
|
|
Addons template.HTML
|
|
}
|
|
|
|
// BillingPageData is the /billing page: the shell plus the invoices region.
|
|
type BillingPageData struct {
|
|
Shell Shell
|
|
Invoices template.HTML
|
|
}
|
|
|
|
// IndexPageData is the / dashboard page: the member's identity, the
|
|
// post-checkout banner state, pending domain claims, the composed
|
|
// workspaces region, and the integration dashboard cards. Named (rather
|
|
// than the handler's former anonymous struct) so the page-anatomy header
|
|
// and section methods in anatomy.go can hang off it.
|
|
type IndexPageData struct {
|
|
Name string
|
|
Username string
|
|
Email string
|
|
KeycloakAccountURL string
|
|
IsOperator bool
|
|
HasMultipleWorkspaces bool
|
|
CheckoutStatus string
|
|
DashboardCards []DashboardCardView
|
|
Workspaces template.HTML
|
|
PendingDomainClaims []PendingDomainClaim
|
|
Shell Shell
|
|
// HasEntitlement reports whether the person's organization holds any
|
|
// entitlement (a numeric or granted-boolean entitlement, or a rung on
|
|
// a plan ladder) — computed alongside DashboardCards in server.go so
|
|
// IsEmpty (server.go) can decide the dashboard's one empty state
|
|
// (member-dashboard ADDED requirement "The dashboard has one empty
|
|
// state"; design D25).
|
|
HasEntitlement bool
|
|
// BillingFacts repeats, on a successful return from Stripe, the
|
|
// sentences the Products page showed under the same rule: who billed
|
|
// the member and what their card statement says (merchant-disclosure
|
|
// D5). Empty on every other load.
|
|
BillingFacts BillingFacts
|
|
}
|