@volter/twin-stripe 3.0.3 → 3.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/journeys/customer-life.json +92 -0
- package/dist/journeys/customer-life.outline.md +4 -0
- package/dist/journeys/decisions.json +7 -1
- package/dist/journeys/demand.json +14 -2
- package/dist/spec/SOURCE.md +9 -0
- package/dist/spec/sources.json +8 -0
- package/dist/src/generated/ui.gen.json +1 -1
- package/dist/src/manifest.js +3 -4
- package/dist/src/screens/dashboard.d.ts +1 -1
- package/dist/src/screens/dashboard.js +61 -3
- package/generated/pack-facts.json +1 -1
- package/journeys/customer-life.json +92 -0
- package/journeys/customer-life.outline.md +4 -0
- package/journeys/decisions.json +7 -1
- package/journeys/demand.json +14 -2
- package/package.json +3 -3
- package/spec/SOURCE.md +9 -0
- package/spec/sources.json +8 -0
- package/src/generated/ui.gen.json +1 -1
- package/src/manifest.ts +3 -4
- package/src/screens/dashboard.tsx +71 -5
|
@@ -5,7 +5,7 @@ import { nowUnix } from "../engine/stripe.js";
|
|
|
5
5
|
import { platformAccountDefault } from "../engine/connect.js";
|
|
6
6
|
import { publicBusinessName } from "../engine/display.js";
|
|
7
7
|
import { PLATFORM_ACCOUNT_ID } from "../engine/stripe.js";
|
|
8
|
-
import { formOf, notFound } from "./shared.js";
|
|
8
|
+
import { formOf, money, notFound } from "./shared.js";
|
|
9
9
|
import { created } from "../semantics/shared.js";
|
|
10
10
|
const redirect = (path) => new Response(null, { status: 303, headers: { location: path } });
|
|
11
11
|
const invalid = (message) => flowPage({ css: [], title: 'Stripe', status: 400, body: _jsx("main", { children: _jsx("p", { role: "alert", children: message }) }) });
|
|
@@ -55,6 +55,19 @@ a { color: #635bff; text-decoration: none; }
|
|
|
55
55
|
.sd-table th { font-size: 12px; font-weight: 600; color: #697386; background: #f6f8fa; }
|
|
56
56
|
.sd-empty { color: #697386; padding: 40px; text-align: center; }
|
|
57
57
|
.sd-code { font-family: ui-monospace,SFMono-Regular,monospace; font-size: 12px; }
|
|
58
|
+
.sd-customer-grid { display: grid; grid-template-columns: minmax(0,1fr) 300px; gap: 32px; align-items: start; }
|
|
59
|
+
.sd-customer-grid > * { min-width: 0; }
|
|
60
|
+
.sd-module { border-bottom: 1px solid #e3e8ee; padding: 0 0 24px; margin-bottom: 24px; overflow-x: auto; }
|
|
61
|
+
.sd-module h2 { font-size: 18px; margin: 0 0 16px; }
|
|
62
|
+
.sd-customer-details { border-left: 1px solid #e3e8ee; padding-left: 24px; overflow-wrap: anywhere; }
|
|
63
|
+
.sd-customer-details dl { margin: 0; }
|
|
64
|
+
.sd-customer-details dt { color: #697386; font-size: 12px; margin-top: 16px; }
|
|
65
|
+
.sd-customer-details dd { margin: 3px 0 0; }
|
|
66
|
+
.sd-customer-filter { display: flex; gap: 8px; margin-bottom: 20px; }
|
|
67
|
+
.sd-customer-filter input { min-width: 0; width: 320px; border: 1px solid #c1c9d2; border-radius: 6px; padding: 8px 10px; }
|
|
68
|
+
.sd-customer-tabs { display: flex; border-bottom: 1px solid #e3e8ee; margin-bottom: 28px; }
|
|
69
|
+
.sd-customer-tabs span { border-bottom: 2px solid #635bff; padding: 0 0 12px; color: #635bff; font-weight: 600; }
|
|
70
|
+
@media(max-width:1000px) { .sd-customer-grid { grid-template-columns: minmax(0,1fr); } .sd-customer-details { border-left: 0; padding: 0; } }
|
|
58
71
|
.sd-dialog { width: min(480px,calc(100vw - 32px)); max-height: calc(100vh - 48px); overflow: auto; border: 1px solid #e3e8ee; border-radius: 8px; padding: 24px; color: #1a1f36; }
|
|
59
72
|
.sd-dialog::backdrop { background: rgba(26,31,54,.4); }
|
|
60
73
|
.sd-dialog h2 { font-size: 20px; margin: 0 0 20px; }
|
|
@@ -69,7 +82,50 @@ a { color: #635bff; text-decoration: none; }
|
|
|
69
82
|
function dashboardPage(ctx, opts) {
|
|
70
83
|
const account = ctx.get('account', PLATFORM_ACCOUNT_ID);
|
|
71
84
|
const links = [['Public details', PUBLIC_DETAILS_PATH], ['Radar lists', '/radar/lists'], ['Issuing balance', '/test/issuing/balance']];
|
|
72
|
-
return flowPage({ title: `${opts.title} – Stripe`, css: [DETAILS_CSS], status: opts.status ?? 200, body: _jsxs("div", { className: "sd", children: [_jsxs("aside", { className: "sd-side", children: [_jsxs("div", { className: "sd-account", children: [publicBusinessName(account), _jsx("span", { className: "sd-mode", children: "Test mode" })] }), _jsxs("nav", { className: "sd-nav", "aria-label": "Dashboard", children: [['Home', 'Balances', 'Transactions', 'Customers', 'Product catalog'].map(label =>
|
|
85
|
+
return flowPage({ title: `${opts.title} – Stripe`, css: [DETAILS_CSS], status: opts.status ?? 200, body: _jsxs("div", { className: "sd", children: [_jsxs("aside", { className: "sd-side", children: [_jsxs("div", { className: "sd-account", children: [publicBusinessName(account), _jsx("span", { className: "sd-mode", children: "Test mode" })] }), _jsxs("nav", { className: "sd-nav", "aria-label": "Dashboard", children: [['Home', 'Balances', 'Transactions', 'Customers', 'Product catalog'].map(label => label === 'Customers'
|
|
86
|
+
? _jsx("a", { href: `${ctx.publicBase}/customers`, "aria-current": opts.path === '/customers' ? 'page' : undefined, children: "Customers" }, label)
|
|
87
|
+
: _jsx("button", { type: "button", disabled: true, title: "Not available in this twin's Dashboard", children: label }, label)), _jsx("p", { className: "sd-nav-title", children: "Shortcuts" }), links.map(([label, path]) => _jsx("a", { href: `${ctx.publicBase}${path}`, "aria-current": opts.path === path ? 'page' : undefined, children: label }, path)), _jsx("p", { className: "sd-nav-title", children: "Products" }), ['Payments', 'Billing', 'Reporting', 'Connect', 'More'].map(label => _jsx("button", { type: "button", disabled: true, title: "Not available in this twin's Dashboard", children: label }, label))] })] }), _jsxs("div", { className: "sd-body", children: [_jsxs("header", { className: "sd-top", children: [_jsx("input", { className: "sd-search", "aria-label": "Search Dashboard", placeholder: "Search", disabled: true, title: "Dashboard search is not available in this twin" }), _jsx("button", { type: "button", disabled: true, title: "Not available in this twin's Dashboard", children: "Create" }), _jsx("button", { type: "button", disabled: true, children: "Help" }), _jsx("a", { href: `${ctx.publicBase}${PUBLIC_DETAILS_PATH}`, children: "Settings" })] }), _jsxs("main", { className: "sd-content", children: [_jsxs("nav", { className: "sd-crumbs", "aria-label": "Breadcrumb", children: [_jsx("span", { children: opts.section }), _jsx("span", { "aria-hidden": "true", children: "/" }), _jsx("span", { children: opts.title })] }), _jsxs("div", { className: "sd-title", children: [_jsx("h1", { children: opts.title }), opts.action] }), opts.body] })] }), _jsx("script", { dangerouslySetInnerHTML: { __html: "document.querySelectorAll('[data-open-dialog]').forEach(b=>b.addEventListener('click',()=>document.getElementById(b.dataset.openDialog).showModal()));document.querySelectorAll('[data-close-dialog]').forEach(b=>b.addEventListener('click',()=>b.closest('dialog').close()));" } })] }) });
|
|
88
|
+
}
|
|
89
|
+
const customerName = (row) => String(row.name || row.email || row.id);
|
|
90
|
+
const resourceId = (value) => typeof value === 'string' ? value : value && typeof value === 'object' ? String(value.id ?? '') : '';
|
|
91
|
+
const newest = (rows) => rows.slice().reverse().sort((a, b) => Number(b.created ?? 0) - Number(a.created ?? 0));
|
|
92
|
+
const customerDate = (value) => typeof value === 'number' && Number.isFinite(value)
|
|
93
|
+
? new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'short', day: 'numeric', timeZone: 'UTC' }).format(new Date(value * 1000)) : '—';
|
|
94
|
+
// source: https://support.stripe.com/questions/export-customer-data-without-the-payment-details
|
|
95
|
+
// source: https://support.stripe.com/questions/updates-to-the-customer-detail-page
|
|
96
|
+
// These public support articles render their body in JavaScript; the source reader gets their shell.
|
|
97
|
+
// Their public rendered body supplies Customers and the dynamic-left/static-right layout (spec/SOURCE.md).
|
|
98
|
+
// Where public references stop: q is an authored name/email/id/description substring filter, not Stripe's
|
|
99
|
+
// private Dashboard search wire. Dates are shown in UTC; browser actions here are read-only. The declared
|
|
100
|
+
// page addresses preserve the customer's returned API id. There is no invented payment history or total.
|
|
101
|
+
function customersPage(ctx, path) {
|
|
102
|
+
if (ctx.call.request.method !== 'GET')
|
|
103
|
+
return notFound();
|
|
104
|
+
if (path === '/customers') {
|
|
105
|
+
const query = new URL(ctx.call.request.url).searchParams.get('q') ?? '';
|
|
106
|
+
const needle = query.trim().toLowerCase();
|
|
107
|
+
const customers = newest(ctx.rows('customer')).filter(row => !needle ||
|
|
108
|
+
[row.name, row.email, row.id, row.description].some(value => typeof value === 'string' && value.toLowerCase().includes(needle)));
|
|
109
|
+
return dashboardPage(ctx, { title: 'Customers', section: 'Customers', path: '/customers',
|
|
110
|
+
body: _jsxs(_Fragment, { children: [_jsxs("form", { className: "sd-customer-filter", method: "get", action: `${ctx.publicBase}/customers`, children: [_jsx("input", { name: "q", "aria-label": "Search customers", placeholder: "Search customers", defaultValue: query }), _jsx("button", { className: "sd-cancel", type: "submit", children: "Search" }), query ? _jsx("a", { href: `${ctx.publicBase}/customers`, children: "Clear" }) : null] }), _jsxs("table", { className: "sd-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Name" }), _jsx("th", { scope: "col", children: "Email" }), _jsx("th", { scope: "col", children: "Description" }), _jsx("th", { scope: "col", children: "Created (UTC)" })] }) }), _jsx("tbody", { children: customers.map(row => _jsxs("tr", { children: [_jsx("td", { children: _jsx("a", { href: `${ctx.publicBase}/customers/${encodeURIComponent(String(row.id))}`, children: customerName(row) }) }), _jsx("td", { children: String(row.email ?? '—') }), _jsx("td", { children: String(row.description ?? '—') }), _jsx("td", { children: customerDate(row.created) })] }, String(row.id))) })] }), !customers.length ? _jsx("p", { className: "sd-empty", children: needle ? 'No customers match your search.' : 'No customers yet.' }) : null] }) });
|
|
111
|
+
}
|
|
112
|
+
let id;
|
|
113
|
+
try {
|
|
114
|
+
id = decodeURIComponent(path.slice('/customers/'.length));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return notFound();
|
|
118
|
+
}
|
|
119
|
+
const customer = ctx.get('customer', id);
|
|
120
|
+
if (!customer || customer.deleted === true)
|
|
121
|
+
return notFound();
|
|
122
|
+
const related = (type) => newest(ctx.rows(type)).filter(row => resourceId(row.customer) === id);
|
|
123
|
+
const payments = related('charge'), subscriptions = related('subscription'), invoices = related('invoice'), methods = related('payment_method');
|
|
124
|
+
const metadata = customer.metadata && typeof customer.metadata === 'object' ? Object.entries(customer.metadata) : [];
|
|
125
|
+
const address = customer.address && typeof customer.address === 'object' ? customer.address : undefined;
|
|
126
|
+
const addressText = address ? ['line1', 'line2', 'city', 'state', 'postal_code', 'country'].map(key => address[key]).filter(Boolean).join(', ') : '—';
|
|
127
|
+
return dashboardPage(ctx, { title: customerName(customer), section: 'Customers', path: '/customers',
|
|
128
|
+
body: _jsxs(_Fragment, { children: [_jsx("a", { href: `${ctx.publicBase}/customers`, children: "\u2190 Customers" }), _jsx("p", { className: "pd-lead sd-code", children: id }), _jsx("div", { className: "sd-customer-tabs", children: _jsx("span", { children: "Overview" }) }), _jsxs("div", { className: "sd-customer-grid", children: [_jsxs("div", { children: [_jsxs("section", { className: "sd-module", children: [_jsx("h2", { children: "Subscriptions" }), subscriptions.length ? _jsxs("table", { className: "sd-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Subscription" }), _jsx("th", { scope: "col", children: "Status" }), _jsx("th", { scope: "col", children: "Created (UTC)" })] }) }), _jsx("tbody", { children: subscriptions.map(row => _jsxs("tr", { children: [_jsx("td", { className: "sd-code", children: String(row.id) }), _jsx("td", { children: String(row.status ?? '—') }), _jsx("td", { children: customerDate(row.created) })] }, String(row.id))) })] }) : _jsx("p", { className: "pd-lead", children: "No subscriptions" })] }), _jsxs("section", { className: "sd-module", children: [_jsx("h2", { children: "Payments" }), payments.length ? _jsxs("table", { className: "sd-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Amount" }), _jsx("th", { scope: "col", children: "Status" }), _jsx("th", { scope: "col", children: "Description" }), _jsx("th", { scope: "col", children: "Created (UTC)" })] }) }), _jsx("tbody", { children: payments.map(row => _jsxs("tr", { children: [_jsx("td", { children: money(row.amount, row.currency) }), _jsx("td", { children: row.refunded === true ? 'Refunded' : String(row.status ?? '—') }), _jsx("td", { children: String(row.description ?? row.id) }), _jsx("td", { children: customerDate(row.created) })] }, String(row.id))) })] }) : _jsx("p", { className: "pd-lead", children: "No payments" })] }), _jsxs("section", { className: "sd-module", children: [_jsx("h2", { children: "Payment methods" }), methods.length ? _jsx("ul", { children: methods.map(row => { const card = row.card && typeof row.card === 'object' ? row.card : undefined; return _jsxs("li", { children: [card ? `${String(card.brand ?? 'Card')} •••• ${String(card.last4 ?? '')}` : String(row.type ?? 'Payment method'), " ", _jsx("span", { className: "sd-code", children: String(row.id) })] }, String(row.id)); }) }) : _jsx("p", { className: "pd-lead", children: "No payment methods" })] }), _jsxs("section", { className: "sd-module", children: [_jsx("h2", { children: "Invoices" }), invoices.length ? _jsxs("table", { className: "sd-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Invoice" }), _jsx("th", { scope: "col", children: "Amount due" }), _jsx("th", { scope: "col", children: "Status" })] }) }), _jsx("tbody", { children: invoices.map(row => _jsxs("tr", { children: [_jsx("td", { children: String(row.number ?? row.id) }), _jsx("td", { children: money(row.amount_due, row.currency) }), _jsx("td", { children: String(row.status ?? '—') })] }, String(row.id))) })] }) : _jsx("p", { className: "pd-lead", children: "No invoices" })] })] }), _jsxs("aside", { className: "sd-customer-details", "aria-label": "Customer details", children: [_jsxs("section", { className: "sd-module", children: [_jsx("h2", { children: "Details" }), _jsxs("dl", { children: [_jsx("dt", { children: "Customer ID" }), _jsx("dd", { className: "sd-code", children: id }), _jsx("dt", { children: "Name" }), _jsx("dd", { children: String(customer.name ?? '—') }), _jsx("dt", { children: "Email" }), _jsx("dd", { children: String(customer.email ?? '—') }), _jsx("dt", { children: "Phone" }), _jsx("dd", { children: String(customer.phone ?? '—') }), _jsx("dt", { children: "Description" }), _jsx("dd", { children: String(customer.description ?? '—') }), _jsx("dt", { children: "Billing address" }), _jsx("dd", { children: addressText }), _jsx("dt", { children: "Created (UTC)" }), _jsx("dd", { children: customerDate(customer.created) })] })] }), _jsxs("section", { className: "sd-module", children: [_jsx("h2", { children: "Metadata" }), metadata.length ? _jsx("dl", { children: metadata.map(([key, value]) => _jsxs("div", { children: [_jsx("dt", { children: key }), _jsx("dd", { children: typeof value === 'string' ? value : JSON.stringify(value) })] }, key)) }) : _jsx("p", { className: "pd-lead", children: "No metadata" })] })] })] })] }) });
|
|
73
129
|
}
|
|
74
130
|
function detailsPage(ctx, account, opts = {}) {
|
|
75
131
|
const named = account?.business_profile?.name;
|
|
@@ -107,12 +163,14 @@ async function publicDetails(ctx) {
|
|
|
107
163
|
// a query-only reference: the page's own path, wherever it is mounted and with or without its trailing slash
|
|
108
164
|
return new Response(null, { status: 303, headers: { location: '?saved=1' } });
|
|
109
165
|
}
|
|
110
|
-
/** Dashboard settings, Radar
|
|
166
|
+
/** Dashboard customer inspection, settings, Radar lists and test Issuing funding. */
|
|
111
167
|
export async function screen(ctx) {
|
|
112
168
|
const path = new URL(ctx.call.request.url).pathname.replace(/\/+$/, '');
|
|
113
169
|
// The declared workspace opens a served setting, not a fabricated analytics home.
|
|
114
170
|
if (!path && ctx.call.request.method === 'GET')
|
|
115
171
|
return redirect(`${ctx.publicBase}${PUBLIC_DETAILS_PATH}`);
|
|
172
|
+
if (path === '/customers' || /^\/customers\/[^/]+$/.test(path))
|
|
173
|
+
return customersPage(ctx, path);
|
|
116
174
|
// source: https://docs.stripe.com/radar/lists "Use the Dashboard or the API to create lists."
|
|
117
175
|
// source: https://docs.stripe.com/radar/lists "Enter a name for the list"
|
|
118
176
|
// source: https://docs.stripe.com/radar/lists "Select the type of list to create."
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
"host": "dashboard.stripe.com",
|
|
35
|
-
"pathPattern": "^/radar/lists(?:/[^/]+)?/?$|^/test/issuing/balance/?$|^/settings/public/?$"
|
|
35
|
+
"pathPattern": "^/customers(?:/[^/]+)?/?$|^/radar/lists(?:/[^/]+)?/?$|^/test/issuing/balance/?$|^/settings/public/?$"
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
38
|
"host": "verify.stripe.com",
|
|
@@ -646,6 +646,51 @@
|
|
|
646
646
|
],
|
|
647
647
|
"evidence": "Dub: apps/web/app/api/workspaces/[idOrSlug]/billing/payment-methods/route.ts:66"
|
|
648
648
|
},
|
|
649
|
+
{
|
|
650
|
+
"name": "Femke finds the SDK-created customer in the Dashboard",
|
|
651
|
+
"method": "GET",
|
|
652
|
+
"path": "https://dashboard.stripe.com/customers?q=billing%40ravel.test",
|
|
653
|
+
"status": 200,
|
|
654
|
+
"expect": [
|
|
655
|
+
{
|
|
656
|
+
"path": "",
|
|
657
|
+
"contains": "billing@ravel.test"
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
"path": "",
|
|
661
|
+
"contains": "/customers/{ravel}"
|
|
662
|
+
}
|
|
663
|
+
],
|
|
664
|
+
"evidence": "World first-use customer inspection; https://support.stripe.com/questions/export-customer-data-without-the-payment-details"
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
"name": "Femke opens Ravel's stored customer profile",
|
|
668
|
+
"method": "GET",
|
|
669
|
+
"path": "https://dashboard.stripe.com/customers/{ravel}",
|
|
670
|
+
"status": 200,
|
|
671
|
+
"expect": [
|
|
672
|
+
{
|
|
673
|
+
"path": "",
|
|
674
|
+
"contains": "billing@ravel.test"
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
"path": "",
|
|
678
|
+
"contains": "Subscriptions"
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
"path": "",
|
|
682
|
+
"contains": "Metadata"
|
|
683
|
+
}
|
|
684
|
+
],
|
|
685
|
+
"evidence": "World first-use customer inspection; https://support.stripe.com/questions/updates-to-the-customer-detail-page"
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
"name": "A missing customer profile stays missing",
|
|
689
|
+
"method": "GET",
|
|
690
|
+
"path": "https://dashboard.stripe.com/customers/cus_missing",
|
|
691
|
+
"status": 404,
|
|
692
|
+
"evidence": "The Dashboard reads an existing customer only"
|
|
693
|
+
},
|
|
649
694
|
{
|
|
650
695
|
"name": "Twenty\u2019s app health check reads its Stripe account with the user-supplied key",
|
|
651
696
|
"method": "GET",
|
|
@@ -3583,6 +3628,53 @@
|
|
|
3583
3628
|
},
|
|
3584
3629
|
"evidence": "Stripe: spec:GetCheckoutSessionsSession"
|
|
3585
3630
|
},
|
|
3631
|
+
{
|
|
3632
|
+
"name": "The app reads the saved lifetime payment with its secret key",
|
|
3633
|
+
"method": "GET",
|
|
3634
|
+
"path": "/v1/payment_intents/{lifetime_pi}",
|
|
3635
|
+
"headers": {
|
|
3636
|
+
"authorization": "Bearer {sk}"
|
|
3637
|
+
},
|
|
3638
|
+
"status": 200,
|
|
3639
|
+
"expect": [
|
|
3640
|
+
{
|
|
3641
|
+
"path": "id",
|
|
3642
|
+
"equals": "{lifetime_pi}"
|
|
3643
|
+
},
|
|
3644
|
+
{
|
|
3645
|
+
"path": "object",
|
|
3646
|
+
"equals": "payment_intent"
|
|
3647
|
+
},
|
|
3648
|
+
{
|
|
3649
|
+
"path": "status",
|
|
3650
|
+
"equals": "succeeded"
|
|
3651
|
+
},
|
|
3652
|
+
{
|
|
3653
|
+
"path": "amount",
|
|
3654
|
+
"equals": 14900
|
|
3655
|
+
},
|
|
3656
|
+
{
|
|
3657
|
+
"path": "amount_received",
|
|
3658
|
+
"equals": 14900
|
|
3659
|
+
},
|
|
3660
|
+
{
|
|
3661
|
+
"path": "currency",
|
|
3662
|
+
"equals": "usd"
|
|
3663
|
+
},
|
|
3664
|
+
{
|
|
3665
|
+
"path": "customer",
|
|
3666
|
+
"equals": "{ravel}"
|
|
3667
|
+
},
|
|
3668
|
+
{
|
|
3669
|
+
"path": "latest_charge",
|
|
3670
|
+
"exists": true
|
|
3671
|
+
}
|
|
3672
|
+
],
|
|
3673
|
+
"capture": {
|
|
3674
|
+
"lifetime_charge": "latest_charge"
|
|
3675
|
+
},
|
|
3676
|
+
"evidence": "Stripe: spec:GetPaymentIntentsIntent; spec:/components/schemas/payment_intent"
|
|
3677
|
+
},
|
|
3586
3678
|
{
|
|
3587
3679
|
"name": "Femke reopens the launch promotion for an alumni webinar",
|
|
3588
3680
|
"method": "POST",
|
|
@@ -55,6 +55,9 @@ Elapsed: 120m.
|
|
|
55
55
|
- and every customer — GET /v1/customers?limit=100. Volter Harness: container/retake/stack/app/src/app.py:116
|
|
56
56
|
- The sample shop's seeder looks its first customer up by email before seeding — GET /v1/customers?email=billing%40ravel.test. Twin: apps/platform/src/sample.ts:40
|
|
57
57
|
- Dub reads Ravel’s billing customer — GET /v1/customers/{ravel}. Dub: apps/web/app/api/workspaces/[idOrSlug]/billing/payment-methods/route.ts:66
|
|
58
|
+
- Femke opens Customers to inspect the SDK-created billing customer — GET https://dashboard.stripe.com/customers?q=billing%40ravel.test. World first-use customer inspection; Stripe: https://support.stripe.com/questions/export-customer-data-without-the-payment-details
|
|
59
|
+
- Femke opens Ravel's profile and sees the stored subscriptions and billing details — GET https://dashboard.stripe.com/customers/{ravel}. World first-use customer inspection; Stripe: https://support.stripe.com/questions/updates-to-the-customer-detail-page
|
|
60
|
+
- A missing customer profile is not an invented customer — GET https://dashboard.stripe.com/customers/cus_missing. The profile reads the existing customer view only.
|
|
58
61
|
- Twenty’s app health check reads its Stripe account with the user-supplied key — GET /v1/account. Twenty: packages/twenty-docs/developers/extend/apps/logic/logic-functions.mdx:850 (GET, Authorization only)
|
|
59
62
|
- Twenty’s invoice workflow creates the quote invoice for Ravel — POST /v1/invoices. Twenty: packages/twenty-docs/user-guide/workflows/how-tos/connect-to-other-tools/generate-quote-or-invoice-from-twenty.mdx:101; packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/http-request/http-request.workflow-action.ts:28
|
|
60
63
|
- The invoice workflow reads the saved draft — GET /v1/invoices/{workflow_invoice}. Postiz: libraries/nestjs-libraries/src/services/stripe.service.ts; Stripe: pinned API and Stripe.js Checkout contract
|
|
@@ -236,6 +239,7 @@ Elapsed: 32d.
|
|
|
236
239
|
- Ravel buys a lifetime upgrade in Checkout — POST /v1/checkout/sessions. Stripe: spec:PostCheckoutSessions
|
|
237
240
|
- The subscriber enters a card — POST {lifetime_cs_url}. Stripe hosted continuation: https://docs.stripe.com/payments/checkout/how-checkout-works
|
|
238
241
|
- The completed lifetime checkout names its payment — GET /v1/checkout/sessions/{lifetime_cs}. Stripe: spec:GetCheckoutSessionsSession
|
|
242
|
+
- The app reads the saved lifetime payment with its secret key — GET /v1/payment_intents/{lifetime_pi}. Stripe: spec:GetPaymentIntentsIntent
|
|
239
243
|
- Femke reopens the launch promotion for an alumni webinar — POST /v1/promotion_codes/{promo}. Dub: apps/web/lib/discounts/discount-provider-stripe.ts:294
|
|
240
244
|
- Ravel buys a third workspace with the reopened promotion — POST /v1/checkout/sessions. Stripe: spec:PostCheckoutSessions
|
|
241
245
|
- The subscriber enters a card — POST {promo_cs_url}. Stripe hosted continuation: https://docs.stripe.com/payments/checkout/how-checkout-works
|
package/journeys/decisions.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"about": "Stripe operations served for the application calls at the cited lines in demand.json
|
|
2
|
+
"about": "Stripe operations served for the application calls at the cited lines in demand.json, for the resource refresh reads declared in src/manifest.ts, and for the completed Checkout payment readback in customer-life.json. The Dashboard customer pages read those same resource views, as declared in src/manifest.ts and customer-life.outline.md. All other operations answer the gap. Each decision names its caller or refresh and its implementation.",
|
|
3
3
|
"decisions": [
|
|
4
4
|
{
|
|
5
5
|
"operation": "DeleteAccountsAccount",
|
|
@@ -247,6 +247,12 @@
|
|
|
247
247
|
"mechanism": "core",
|
|
248
248
|
"why": "the derived core's list of payment_intent: a plain list the manifest's declaration serves"
|
|
249
249
|
},
|
|
250
|
+
{
|
|
251
|
+
"operation": "GetPaymentIntentsIntent",
|
|
252
|
+
"demand": "Completed Checkout payment readback (journeys/customer-life.json)",
|
|
253
|
+
"mechanism": "core",
|
|
254
|
+
"why": "Stripe SDK paymentIntents.retrieve reads the PaymentIntent named by the completed Checkout session: id, succeeded status, collected amount, currency and latest_charge come from the existing stored payment_intent through the derived core, as spec:GetPaymentIntentsIntent retrieves a PaymentIntent previously created. This life uses the secret key; client-side publishable-key retrieval remains outside this act and the existing credential refusal is unchanged."
|
|
255
|
+
},
|
|
250
256
|
{
|
|
251
257
|
"operation": "GetPaymentMethods",
|
|
252
258
|
"demand": "Dub, Postiz (journeys/demand.json)",
|
package/journeys/demand.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"measured": "Applications are read at the scanned commits below. Demand includes SDK methods, delegated Stripe.js calls, raw URLs and API-path strings, and shipped guides that use a shipped feature. Credentials authorize a feature; they do not opt into it. Postiz ships Stripe as its web billing provider. Twenty workflow HTTP actions and enterprise website billing are independent of the server billing flag (which defaults false). Cal payment/OAuth needs an app-store install; its metadata installed flag makes the catalog entry available, while the user install requires a credential created by the install callback. All products in generated/DEMAND.md
|
|
2
|
+
"measured": "Applications are read at the scanned commits below. Demand includes SDK methods, delegated Stripe.js calls, raw URLs and API-path strings, and shipped guides that use a shipped feature. Credentials authorize a feature; they do not opt into it. Postiz ships Stripe as its web billing provider. Twenty workflow HTTP actions and enterprise website billing are independent of the server billing flag (which defaults false). Cal payment/OAuth needs an app-store install; its metadata installed flag makes the catalog entry available, while the user install requires a credential created by the install callback. All products in generated/DEMAND.md\u2019s Volter column were searched, including their own World scenarios and seeds; none calls a removed operation. The launch does not name Stripe. Separately invoked Rallly billing CLI and Dub maintenance scripts are outside the running products. Dub\u2019s raw GET /v1/mandates, POST /v1/fx_quotes and v2 calls remain outside the pinned v1 document and answer the vendor gap; their demand is retained, not classified optional.",
|
|
3
3
|
"applications": [
|
|
4
4
|
{
|
|
5
5
|
"name": "Open Autonomy",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"client": "fetch (the platform's sample World seeder)",
|
|
66
66
|
"backend": [
|
|
67
67
|
{
|
|
68
|
-
"call": "call('stripe', '/v1/customers?email
|
|
68
|
+
"call": "call('stripe', '/v1/customers?email=\u2026')",
|
|
69
69
|
"operation": "GetCustomers",
|
|
70
70
|
"evidence": "apps/platform/src/sample.ts:40"
|
|
71
71
|
},
|
|
@@ -687,6 +687,18 @@
|
|
|
687
687
|
}
|
|
688
688
|
],
|
|
689
689
|
"gate": "The shipped website enterprise checkout directly uses Stripe independently of the server IS_BILLING_ENABLED=false switch (packages/twenty-website/src/app/api/enterprise/checkout/route.ts:7-19,51)."
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
"name": "World first-use",
|
|
693
|
+
"commit": "7638d09b23a3bb36067a607e0fd19898b0a5b624",
|
|
694
|
+
"client": "Real Stripe SDK customer workflow and normal browser inspection",
|
|
695
|
+
"frontend": [
|
|
696
|
+
{
|
|
697
|
+
"call": "Customers sidebar \u2192 SDK-created customer profile",
|
|
698
|
+
"operation": "GET https://dashboard.stripe.com/customers; GET https://dashboard.stripe.com/customers/{customer}",
|
|
699
|
+
"evidence": "twin-world/docs/contributing/first-use.md:39; twin-world/docs/getting-started.md:158"
|
|
700
|
+
}
|
|
701
|
+
]
|
|
690
702
|
}
|
|
691
703
|
],
|
|
692
704
|
"optional": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/twin-stripe",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.5",
|
|
4
4
|
"description": "Local Stripe customer, checkout and billing workflows with synthetic state and the real Stripe SDK. Supported operations and hosted-page limits are documented in the README.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
"world-stripe": "dist/src/cli.js"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@volter/world-ui": "^1.0.
|
|
49
|
+
"@volter/world-ui": "^1.0.155",
|
|
50
50
|
"react": "^19.2.7",
|
|
51
51
|
"react-dom": "^19.2.7"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@volter/world-core": "^3.0.
|
|
54
|
+
"@volter/world-core": "^3.0.155"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/bun": "^1.2.20",
|
package/spec/SOURCE.md
CHANGED
|
@@ -8,6 +8,15 @@
|
|
|
8
8
|
|
|
9
9
|
The pack serves this document's version (`SERVED_VERSION` in `src/engine/version.ts`).
|
|
10
10
|
|
|
11
|
+
Customer Dashboard references, read 2026-10-09: [Export customer data](https://support.stripe.com/questions/export-customer-data-without-the-payment-details)
|
|
12
|
+
establishes the Customers section at `/customers`. [Updates to the Customer details page](https://support.stripe.com/questions/updates-to-the-customer-detail-page)
|
|
13
|
+
establishes dynamic payments, invoices and subscriptions in the primary column, with details and metadata on the right.
|
|
14
|
+
[Manage customer information](https://support.stripe.com/questions/manage-a-customer-s-information-and-payments)
|
|
15
|
+
names the customer profile and payment-method modules; its older left-details layout is superseded by the update article.
|
|
16
|
+
The pages publish no version date. The authored list and read-only detail use stored API views, UTC dates and an explicitly
|
|
17
|
+
limited name/email/id/description search. Dashboard create/edit actions, exports and guest-customer grouping are outside this scope.
|
|
18
|
+
Only public documentation was read; no vendor sign-in, upstream DOM, script or image is shipped.
|
|
19
|
+
|
|
11
20
|
- **Published examples:** `fixtures3.json.gz`, gzip of Stripe's `openapi/fixtures3.json` (one sample object per resource,
|
|
12
21
|
the objects Stripe's API reference shows), from commit `0d98342fc985dc7710a44be25e8528dd8e099bc2` (2026-09-23).
|
|
13
22
|
SHA-256 of the uncompressed file: `e6822f7ee6fd71bdde6228537368aafcfb7191109340fe119d9aca27c6e9396d`.
|
package/spec/sources.json
CHANGED
|
@@ -302,5 +302,13 @@
|
|
|
302
302
|
"quotes": {
|
|
303
303
|
"Unrecognized request URL (GET: /v1/test).": true
|
|
304
304
|
}
|
|
305
|
+
},
|
|
306
|
+
"https://support.stripe.com/questions/export-customer-data-without-the-payment-details": {
|
|
307
|
+
"status": 200,
|
|
308
|
+
"checked": "2026-10-09"
|
|
309
|
+
},
|
|
310
|
+
"https://support.stripe.com/questions/updates-to-the-customer-detail-page": {
|
|
311
|
+
"status": 200,
|
|
312
|
+
"checked": "2026-10-09"
|
|
305
313
|
}
|
|
306
314
|
}
|