@funnelsgrove/cli 0.1.315 → 0.1.316
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 +12 -8
- package/dist/cli.js +54 -26
- package/dist/{customerCommands.d.ts → userCommands.d.ts} +48 -20
- package/dist/{customerCommands.js → userCommands.js} +53 -8
- package/funnel-contract-compatibility.json +28 -28
- package/package.json +1 -1
- package/template_docs/.funnelsgrove-docs.json +3 -3
- package/template_docs/docs/funnelsgrove/migrations/step-contract-v3.md +1 -1
- package/template_docs/funnel-docs.config.json +1 -1
- package/template_scaffold/.funnelsgrove-docs.json +3 -3
- package/template_scaffold/.funnelsgrove-scaffold.json +5 -5
- package/template_scaffold/docs/funnelsgrove/migrations/step-contract-v3.md +1 -1
- package/template_scaffold/funnel-docs.config.json +1 -1
package/README.md
CHANGED
|
@@ -243,18 +243,22 @@ by Merchanto; the cost lines cover FunnelsGrove alerts for the whole project.
|
|
|
243
243
|
The output never includes customer emails, names, phone numbers, IP addresses,
|
|
244
244
|
card digits, network references or Stripe identifiers.
|
|
245
245
|
|
|
246
|
-
Support a
|
|
247
|
-
user ID, refund chosen payments and cancel
|
|
246
|
+
Support a user (workspace admins only): find the projects they belong to,
|
|
247
|
+
look them up by email or funnel user ID, refund chosen payments and cancel
|
|
248
|
+
their subscription:
|
|
248
249
|
|
|
249
250
|
```bash
|
|
250
|
-
fgrove
|
|
251
|
-
fgrove
|
|
252
|
-
fgrove
|
|
253
|
-
fgrove
|
|
251
|
+
fgrove users find --email buyer@example.com --json # every project you administer where this user exists, each with its lookup
|
|
252
|
+
fgrove users lookup --project claimbee --email buyer@example.com --json # users, live subscriptions, payments with refund eligibility
|
|
253
|
+
fgrove users refund --project claimbee --email buyer@example.com --payment ch_123 --payment pay_456 --dry-run
|
|
254
|
+
fgrove users refund --project claimbee --email buyer@example.com --payment ch_123 --payment pay_456
|
|
255
|
+
fgrove users cancel --project claimbee --user-id 8f1c2d # at period end; --now cancels immediately without refunding the remainder
|
|
254
256
|
```
|
|
255
257
|
|
|
256
|
-
Pass exactly one of `--email` or `--user-id`; `--env test` reads test-mode
|
|
257
|
-
|
|
258
|
+
`customers` remains an alias of `users`. Pass exactly one of `--email` or `--user-id`; `--env test` reads test-mode
|
|
259
|
+
users. `find` searches every workspace you belong to (narrow it with
|
|
260
|
+
`--workspace`), lists workspaces where you are not an admin as not searched and
|
|
261
|
+
exits 1 when a project could not be searched. Stripe charges and subscriptions come live from Stripe; Solidgate
|
|
258
262
|
payments come from the FunnelsGrove ledger with refunded amounts read live from
|
|
259
263
|
Solidgate. Refunds are full refunds of each payment's remaining amount, using
|
|
260
264
|
the payment IDs printed by `lookup`. Each payment is refunded at most once,
|
package/dist/cli.js
CHANGED
|
@@ -20,7 +20,7 @@ import { executeFastDraftPatchSync, uploadDirectAssetWithFetch } from './draftPa
|
|
|
20
20
|
import { updateCliInstallations } from './selfUpdate.js';
|
|
21
21
|
import { executeExpensePublish, executeExpensePull } from './expenseCommands.js';
|
|
22
22
|
import { buildAlertStatisticsReport, fetchAlertStatistics, fetchPreventSummary, formatAlertRows, formatAlertStatistics, formatPreventRows, formatPreventSummary, listAlerts, listPreventSearches, parseAlertMonth, toAlertJsonRow, } from './alertCommands.js';
|
|
23
|
-
import {
|
|
23
|
+
import { cancelUserSubscription, findUser, formatUserCancellation, formatUserFind, formatUserLookup, formatUserRefunds, formatRefundPreview, lookupUser, parseUserSelector, previewRefund, refundUserPayments, } from './userCommands.js';
|
|
24
24
|
import { executeOfferSetPrices, executeOfferSetPublish, executeOfferSetPull, formatOfferSetPriceCatalogs, } from './offerSetCommands.js';
|
|
25
25
|
import { executeEmailPull, executeEmailPush, executeEmailSend, executeEmailUpdateVariables, executeEmailSequenceAddStep, executeEmailSequenceCancel, executeEmailSequenceCreate, executeEmailSequencePublish, executeEmailSequenceSetActive, executeEmailLibraryAdd, executeEmailLibraryList, executeEmailTemplatePublish, executeEmailValidate, parseEmailVariablesJson, } from './emailCommands.js';
|
|
26
26
|
import { formatGitHubConnectRows, formatGitHubJobRows, formatGitHubStatusRows, } from './githubOutput.js';
|
|
@@ -2285,71 +2285,99 @@ addAlertScopeOptions(preventCommand
|
|
|
2285
2285
|
}
|
|
2286
2286
|
printValueRows(formatPreventSummary(month, summary).map((line) => [line]));
|
|
2287
2287
|
});
|
|
2288
|
-
const
|
|
2288
|
+
const addUserOptions = (command) => command
|
|
2289
2289
|
.option('--workspace <id-or-slug-or-name>', 'Workspace id, slug, or name')
|
|
2290
2290
|
.option('--project <id-or-slug-or-name>', 'Project id, slug, or name')
|
|
2291
|
-
.option('--email <email>', '
|
|
2291
|
+
.option('--email <email>', 'User email (exact match, case-insensitive)')
|
|
2292
2292
|
.option('--user-id <id>', 'Funnel user ID (the SDK user ID)')
|
|
2293
2293
|
.option('--env <environment>', 'test or live', 'live')
|
|
2294
2294
|
.option('--json', 'Print machine-readable JSON');
|
|
2295
|
-
const
|
|
2296
|
-
const selector =
|
|
2295
|
+
const resolveUserCommandScope = async (options) => {
|
|
2296
|
+
const selector = parseUserSelector(options);
|
|
2297
2297
|
return { scope: await resolveProjectCommandScope(options), selector };
|
|
2298
2298
|
};
|
|
2299
|
-
const
|
|
2300
|
-
'fgrove
|
|
2301
|
-
'fgrove
|
|
2302
|
-
'fgrove
|
|
2299
|
+
const usersCommand = addExamples(program.command('users').alias('customers').description('Look up a user\'s subscriptions and payments, refund payments and cancel subscriptions (workspace admins)'), [
|
|
2300
|
+
'fgrove users find --email buyer@example.com',
|
|
2301
|
+
'fgrove users lookup --project claimbee --email buyer@example.com',
|
|
2302
|
+
'fgrove users refund --project claimbee --email buyer@example.com --payment ch_123 --payment pay_456',
|
|
2303
|
+
'fgrove users cancel --project claimbee --user-id 8f1c... --now',
|
|
2303
2304
|
]);
|
|
2304
|
-
|
|
2305
|
+
usersCommand
|
|
2306
|
+
.command('find')
|
|
2307
|
+
.description('Find every project you administer where the user exists, with their subscriptions and payments')
|
|
2308
|
+
.option('--workspace <id-or-slug-or-name>', 'Search only this workspace (default: every workspace you belong to)')
|
|
2309
|
+
.option('--email <email>', 'User email (exact match, case-insensitive)')
|
|
2310
|
+
.option('--user-id <id>', 'Funnel user ID (the SDK user ID)')
|
|
2311
|
+
.option('--env <environment>', 'test or live', 'live')
|
|
2312
|
+
.option('--json', 'Print machine-readable JSON')
|
|
2313
|
+
.action(async (options) => {
|
|
2314
|
+
const selector = parseUserSelector(options);
|
|
2315
|
+
const token = await readAuthToken();
|
|
2316
|
+
const workspaces = options.workspace
|
|
2317
|
+
? [await resolveWorkspace(token, options.workspace)]
|
|
2318
|
+
: (await getCurrentUser(token)).workspaces;
|
|
2319
|
+
const projects = (await Promise.all(workspaces.map(async (workspace) => ((await listProjects(token, workspace.id)).map((project) => ({
|
|
2320
|
+
workspace: { id: workspace.id, name: workspace.name, slug: workspace.slug },
|
|
2321
|
+
project: { id: project.id, name: project.name, slug: project.slug },
|
|
2322
|
+
})))))).flat();
|
|
2323
|
+
const found = await findUser({ callApi, token, projects }, selector);
|
|
2324
|
+
if (found.skipped.some((skip) => skip.reason === 'failed'))
|
|
2325
|
+
process.exitCode = 1;
|
|
2326
|
+
if (options.json) {
|
|
2327
|
+
console.log(JSON.stringify(found, null, 2));
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
printValueRows(formatUserFind(found).map((line) => [line]));
|
|
2331
|
+
});
|
|
2332
|
+
addUserOptions(usersCommand
|
|
2305
2333
|
.command('lookup')
|
|
2306
|
-
.description('List the
|
|
2334
|
+
.description('List the matching funnel users, their live provider subscriptions and payments with refund eligibility'))
|
|
2307
2335
|
.action(async (options) => {
|
|
2308
|
-
const { scope, selector } = await
|
|
2309
|
-
const lookup = await
|
|
2336
|
+
const { scope, selector } = await resolveUserCommandScope(options);
|
|
2337
|
+
const lookup = await lookupUser(scope, selector);
|
|
2310
2338
|
if (options.json) {
|
|
2311
2339
|
console.log(JSON.stringify(lookup, null, 2));
|
|
2312
2340
|
return;
|
|
2313
2341
|
}
|
|
2314
|
-
printValueRows(
|
|
2342
|
+
printValueRows(formatUserLookup(lookup).map((line) => [line]));
|
|
2315
2343
|
});
|
|
2316
|
-
|
|
2344
|
+
addUserOptions(usersCommand
|
|
2317
2345
|
.command('refund')
|
|
2318
|
-
.description('Fully refund the remaining amount of each chosen payment (payment IDs from
|
|
2319
|
-
.requiredOption('--payment <id>', 'Payment ID from
|
|
2346
|
+
.description('Fully refund the remaining amount of each chosen payment (payment IDs from users lookup)')
|
|
2347
|
+
.requiredOption('--payment <id>', 'Payment ID from users lookup; repeat for several', (value, previous = []) => [...previous, value])
|
|
2320
2348
|
.option('--dry-run', 'Show what would be refunded without refunding'))
|
|
2321
2349
|
.action(async (options) => {
|
|
2322
|
-
const { scope, selector } = await
|
|
2350
|
+
const { scope, selector } = await resolveUserCommandScope(options);
|
|
2323
2351
|
if (options.dryRun) {
|
|
2324
|
-
const preview = previewRefund(await
|
|
2352
|
+
const preview = previewRefund(await lookupUser(scope, selector), options.payment);
|
|
2325
2353
|
console.log(options.json ? JSON.stringify({ dryRun: true, refunds: preview }, null, 2)
|
|
2326
2354
|
: formatRefundPreview(preview).join('\n'));
|
|
2327
2355
|
return;
|
|
2328
2356
|
}
|
|
2329
|
-
const { refunds } = await
|
|
2357
|
+
const { refunds } = await refundUserPayments(scope, selector, options.payment);
|
|
2330
2358
|
if (refunds.some((refund) => refund.outcome === 'failed'))
|
|
2331
2359
|
process.exitCode = 1;
|
|
2332
2360
|
if (options.json) {
|
|
2333
2361
|
console.log(JSON.stringify({ refunds }, null, 2));
|
|
2334
2362
|
return;
|
|
2335
2363
|
}
|
|
2336
|
-
printValueRows(
|
|
2364
|
+
printValueRows(formatUserRefunds(refunds).map((line) => [line]));
|
|
2337
2365
|
});
|
|
2338
|
-
|
|
2366
|
+
addUserOptions(usersCommand
|
|
2339
2367
|
.command('cancel')
|
|
2340
|
-
.description('Cancel the
|
|
2368
|
+
.description('Cancel the user\'s billing subscription at period end, or immediately with --now')
|
|
2341
2369
|
.option('--subscription <id>', 'Subscription ID; required when several subscriptions are still billing')
|
|
2342
2370
|
.option('--now', 'Cancel immediately; the paid remainder is forfeited without a refund'))
|
|
2343
2371
|
.action(async (options) => {
|
|
2344
|
-
const { scope, selector } = await
|
|
2372
|
+
const { scope, selector } = await resolveUserCommandScope(options);
|
|
2345
2373
|
if (options.now)
|
|
2346
2374
|
console.error('Cancelling immediately: access ends now and the paid remainder is not refunded.');
|
|
2347
|
-
const result = await
|
|
2375
|
+
const result = await cancelUserSubscription(scope, selector, { subscriptionId: options.subscription, now: options.now });
|
|
2348
2376
|
if (options.json) {
|
|
2349
2377
|
console.log(JSON.stringify(result, null, 2));
|
|
2350
2378
|
return;
|
|
2351
2379
|
}
|
|
2352
|
-
printValueRows(
|
|
2380
|
+
printValueRows(formatUserCancellation(result).map((line) => [line]));
|
|
2353
2381
|
});
|
|
2354
2382
|
const funnelsCommand = addExamples(program.command('funnels').description('Manage funnels'), [
|
|
2355
2383
|
'fgrove funnels list',
|
|
@@ -4,19 +4,19 @@ type CallApi = <T>(input: {
|
|
|
4
4
|
data?: unknown;
|
|
5
5
|
token?: string | null;
|
|
6
6
|
}) => Promise<T>;
|
|
7
|
-
export type
|
|
7
|
+
export type UserCommandScope = {
|
|
8
8
|
callApi: CallApi;
|
|
9
9
|
token: string;
|
|
10
10
|
workspaceId: string;
|
|
11
11
|
projectId: string;
|
|
12
12
|
};
|
|
13
|
-
export type
|
|
13
|
+
export type UserSelector = {
|
|
14
14
|
environment: 'test' | 'live';
|
|
15
15
|
email?: string;
|
|
16
16
|
userId?: string;
|
|
17
17
|
};
|
|
18
18
|
type Provider = 'stripe' | 'solidgate';
|
|
19
|
-
export type
|
|
19
|
+
export type UserSubscription = {
|
|
20
20
|
provider: Provider;
|
|
21
21
|
paymentProfileId: string;
|
|
22
22
|
subscriptionId: string;
|
|
@@ -31,7 +31,7 @@ export type CustomerSubscription = {
|
|
|
31
31
|
interval: string | null;
|
|
32
32
|
billing: boolean;
|
|
33
33
|
};
|
|
34
|
-
export type
|
|
34
|
+
export type UserPayment = {
|
|
35
35
|
provider: Provider;
|
|
36
36
|
paymentProfileId: string;
|
|
37
37
|
paymentId: string | null;
|
|
@@ -45,7 +45,7 @@ export type CustomerPayment = {
|
|
|
45
45
|
refundable: boolean;
|
|
46
46
|
notRefundableReason: string | null;
|
|
47
47
|
};
|
|
48
|
-
export type
|
|
48
|
+
export type UserLookup = {
|
|
49
49
|
environment: 'test' | 'live';
|
|
50
50
|
users: Array<{
|
|
51
51
|
funnelUserId: string;
|
|
@@ -54,8 +54,8 @@ export type CustomerLookup = {
|
|
|
54
54
|
funnelId: string | null;
|
|
55
55
|
createdAt: string;
|
|
56
56
|
}>;
|
|
57
|
-
subscriptions:
|
|
58
|
-
payments:
|
|
57
|
+
subscriptions: UserSubscription[];
|
|
58
|
+
payments: UserPayment[];
|
|
59
59
|
sources: Array<{
|
|
60
60
|
provider: string;
|
|
61
61
|
paymentProfileId: string | null;
|
|
@@ -63,7 +63,7 @@ export type CustomerLookup = {
|
|
|
63
63
|
detail?: string;
|
|
64
64
|
}>;
|
|
65
65
|
};
|
|
66
|
-
export type
|
|
66
|
+
export type UserRefund = {
|
|
67
67
|
paymentId: string;
|
|
68
68
|
provider: Provider | null;
|
|
69
69
|
outcome: 'refunded' | 'already_refunded' | 'processing' | 'failed';
|
|
@@ -72,7 +72,7 @@ export type CustomerRefund = {
|
|
|
72
72
|
refundIds: string[];
|
|
73
73
|
error?: string;
|
|
74
74
|
};
|
|
75
|
-
export type
|
|
75
|
+
export type UserCancellation = {
|
|
76
76
|
provider: Provider;
|
|
77
77
|
paymentProfileId: string;
|
|
78
78
|
subscriptionId: string;
|
|
@@ -84,21 +84,49 @@ export type CustomerCancellation = {
|
|
|
84
84
|
paidDaysForfeited: number;
|
|
85
85
|
};
|
|
86
86
|
/** Exactly one of email or user ID; emails are matched trimmed and lowercased. */
|
|
87
|
-
export declare const
|
|
87
|
+
export declare const parseUserSelector: (options: {
|
|
88
88
|
email?: string;
|
|
89
89
|
userId?: string;
|
|
90
90
|
env?: string;
|
|
91
|
-
}) =>
|
|
92
|
-
export declare const
|
|
93
|
-
export declare const
|
|
94
|
-
refunds:
|
|
91
|
+
}) => UserSelector;
|
|
92
|
+
export declare const lookupUser: (scope: UserCommandScope, selector: UserSelector) => Promise<UserLookup>;
|
|
93
|
+
export declare const refundUserPayments: (scope: UserCommandScope, selector: UserSelector, paymentIds: string[]) => Promise<{
|
|
94
|
+
refunds: UserRefund[];
|
|
95
95
|
}>;
|
|
96
|
-
export declare const
|
|
96
|
+
export declare const cancelUserSubscription: (scope: UserCommandScope, selector: UserSelector, input: {
|
|
97
97
|
subscriptionId?: string;
|
|
98
98
|
now?: boolean;
|
|
99
|
-
}) => Promise<
|
|
99
|
+
}) => Promise<UserCancellation>;
|
|
100
|
+
type NamedScope = {
|
|
101
|
+
id: string;
|
|
102
|
+
name: string;
|
|
103
|
+
slug: string;
|
|
104
|
+
};
|
|
105
|
+
export type UserSearchProject = {
|
|
106
|
+
workspace: NamedScope;
|
|
107
|
+
project: NamedScope;
|
|
108
|
+
};
|
|
109
|
+
export type UserFind = {
|
|
110
|
+
environment: 'test' | 'live';
|
|
111
|
+
searchedProjects: number;
|
|
112
|
+
matches: Array<UserSearchProject & {
|
|
113
|
+
lookup: UserLookup;
|
|
114
|
+
}>;
|
|
115
|
+
skipped: Array<UserSearchProject & {
|
|
116
|
+
reason: 'not-admin' | 'failed';
|
|
117
|
+
detail?: string;
|
|
118
|
+
}>;
|
|
119
|
+
};
|
|
120
|
+
/** Runs users lookup in every given project; projects where the user is unknown are left out. */
|
|
121
|
+
export declare const findUser: (input: {
|
|
122
|
+
callApi: CallApi;
|
|
123
|
+
token: string;
|
|
124
|
+
projects: UserSearchProject[];
|
|
125
|
+
concurrency?: number;
|
|
126
|
+
}, selector: UserSelector) => Promise<UserFind>;
|
|
127
|
+
export declare const formatUserFind: (found: UserFind) => string[];
|
|
100
128
|
/** Payments the refund would touch, in the order requested; unknown IDs are reported, not dropped. */
|
|
101
|
-
export declare const previewRefund: (lookup:
|
|
129
|
+
export declare const previewRefund: (lookup: UserLookup, paymentIds: string[]) => ({
|
|
102
130
|
paymentId: string;
|
|
103
131
|
provider: Provider;
|
|
104
132
|
amountMinor: number;
|
|
@@ -114,7 +142,7 @@ export declare const previewRefund: (lookup: CustomerLookup, paymentIds: string[
|
|
|
114
142
|
reason: string;
|
|
115
143
|
})[];
|
|
116
144
|
export declare const formatRefundPreview: (preview: ReturnType<typeof previewRefund>) => string[];
|
|
117
|
-
export declare const
|
|
118
|
-
export declare const
|
|
119
|
-
export declare const
|
|
145
|
+
export declare const formatUserLookup: (lookup: UserLookup) => string[];
|
|
146
|
+
export declare const formatUserRefunds: (refunds: UserRefund[]) => string[];
|
|
147
|
+
export declare const formatUserCancellation: (result: UserCancellation) => string[];
|
|
120
148
|
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { formatMinor } from './alertCommands.js';
|
|
2
|
+
import { TrpcApiError } from './apiClient.js';
|
|
2
3
|
/** Exactly one of email or user ID; emails are matched trimmed and lowercased. */
|
|
3
|
-
export const
|
|
4
|
+
export const parseUserSelector = (options) => {
|
|
4
5
|
const email = options.email?.trim().toLowerCase();
|
|
5
6
|
const userId = options.userId?.trim();
|
|
6
7
|
if (Boolean(email) === Boolean(userId))
|
|
@@ -12,30 +13,74 @@ export const parseCustomerSelector = (options) => {
|
|
|
12
13
|
const request = (scope, selector) => ({
|
|
13
14
|
workspaceId: scope.workspaceId, projectId: scope.projectId, ...selector,
|
|
14
15
|
});
|
|
15
|
-
export const
|
|
16
|
+
export const lookupUser = (scope, selector) => scope.callApi({
|
|
16
17
|
path: 'customers.lookup', type: 'query', token: scope.token, data: request(scope, selector),
|
|
17
18
|
});
|
|
18
|
-
export const
|
|
19
|
+
export const refundUserPayments = (scope, selector, paymentIds) => (scope.callApi({
|
|
19
20
|
path: 'customers.refund', type: 'mutation', token: scope.token, data: { ...request(scope, selector), paymentIds },
|
|
20
21
|
}));
|
|
21
|
-
export const
|
|
22
|
+
export const cancelUserSubscription = (scope, selector, input) => scope.callApi({
|
|
22
23
|
path: 'customers.cancel',
|
|
23
24
|
type: 'mutation',
|
|
24
25
|
token: scope.token,
|
|
25
26
|
data: { ...request(scope, selector), ...(input.subscriptionId ? { subscriptionId: input.subscriptionId } : {}), mode: input.now ? 'immediate' : 'period_end' },
|
|
26
27
|
});
|
|
28
|
+
/** Runs users lookup in every given project; projects where the user is unknown are left out. */
|
|
29
|
+
export const findUser = async (input, selector) => {
|
|
30
|
+
const result = { environment: selector.environment, searchedProjects: input.projects.length, matches: [], skipped: [] };
|
|
31
|
+
const queue = [...input.projects];
|
|
32
|
+
const worker = async () => {
|
|
33
|
+
for (let target = queue.shift(); target; target = queue.shift()) {
|
|
34
|
+
const scope = { callApi: input.callApi, token: input.token, workspaceId: target.workspace.id, projectId: target.project.id };
|
|
35
|
+
try {
|
|
36
|
+
result.matches.push({ ...target, lookup: await lookupUser(scope, selector) });
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
const code = error instanceof TrpcApiError ? error.code : null;
|
|
40
|
+
if (code === 'NOT_FOUND')
|
|
41
|
+
continue;
|
|
42
|
+
result.skipped.push(code === 'FORBIDDEN'
|
|
43
|
+
? { ...target, reason: 'not-admin' }
|
|
44
|
+
: { ...target, reason: 'failed', detail: error instanceof Error ? error.message : String(error) });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
await Promise.all(Array.from({ length: Math.min(input.concurrency ?? 4, input.projects.length) }, worker));
|
|
49
|
+
const order = new Map(input.projects.map((target, index) => [target.project.id, index]));
|
|
50
|
+
const byInput = (a, b) => (order.get(a.project.id) ?? 0) - (order.get(b.project.id) ?? 0);
|
|
51
|
+
result.matches.sort(byInput);
|
|
52
|
+
result.skipped.sort(byInput);
|
|
53
|
+
return result;
|
|
54
|
+
};
|
|
55
|
+
export const formatUserFind = (found) => {
|
|
56
|
+
const lines = [`Environment\t${found.environment}`, `Searched\t${found.searchedProjects} projects`, `Found in\t${found.matches.length} projects`];
|
|
57
|
+
for (const match of found.matches) {
|
|
58
|
+
lines.push('', `== ${match.project.name} (${match.project.slug})\tworkspace ${match.workspace.name}`, ...formatUserLookup(match.lookup).slice(1));
|
|
59
|
+
}
|
|
60
|
+
const notAdmin = [...new Set(found.skipped.filter((skip) => skip.reason === 'not-admin').map((skip) => skip.workspace.name))];
|
|
61
|
+
const failed = found.skipped.filter((skip) => skip.reason === 'failed');
|
|
62
|
+
if (notAdmin.length || failed.length)
|
|
63
|
+
lines.push('');
|
|
64
|
+
if (notAdmin.length)
|
|
65
|
+
lines.push(`Not searched (not a workspace admin)\t${notAdmin.join(', ')}`);
|
|
66
|
+
for (const skip of failed)
|
|
67
|
+
lines.push(`Failed\t${skip.project.name} (${skip.project.slug})\t${skip.detail ?? ''}`);
|
|
68
|
+
if (failed.length)
|
|
69
|
+
lines.push('Incomplete: some projects did not answer; the user may also be in those projects.');
|
|
70
|
+
return lines;
|
|
71
|
+
};
|
|
27
72
|
/** Payments the refund would touch, in the order requested; unknown IDs are reported, not dropped. */
|
|
28
73
|
export const previewRefund = (lookup, paymentIds) => paymentIds.map((paymentId) => {
|
|
29
74
|
const payment = lookup.payments.find((candidate) => candidate.paymentId === paymentId);
|
|
30
75
|
return payment
|
|
31
76
|
? { paymentId, provider: payment.provider, amountMinor: payment.amountMinor - payment.refundedMinor, currency: payment.currency, refundable: payment.refundable, reason: payment.notRefundableReason }
|
|
32
|
-
: { paymentId, provider: null, amountMinor: 0, currency: null, refundable: false, reason: 'Not found in
|
|
77
|
+
: { paymentId, provider: null, amountMinor: 0, currency: null, refundable: false, reason: 'Not found in users lookup' };
|
|
33
78
|
});
|
|
34
79
|
export const formatRefundPreview = (preview) => preview.map((row) => [
|
|
35
80
|
row.paymentId, row.refundable ? `would refund ${formatMinor(row.amountMinor, row.currency)}` : `skip: ${row.reason}`,
|
|
36
81
|
].join('\t'));
|
|
37
82
|
const day = (value) => value?.slice(0, 10) ?? '';
|
|
38
|
-
export const
|
|
83
|
+
export const formatUserLookup = (lookup) => {
|
|
39
84
|
const lines = [`Environment\t${lookup.environment}`];
|
|
40
85
|
for (const user of lookup.users)
|
|
41
86
|
lines.push(`User\t${user.userId ?? ''}\t${user.email ?? ''}\tfunnel user ${user.funnelUserId}\tcreated ${day(user.createdAt)}`);
|
|
@@ -59,11 +104,11 @@ export const formatCustomerLookup = (lookup) => {
|
|
|
59
104
|
}
|
|
60
105
|
return lines;
|
|
61
106
|
};
|
|
62
|
-
export const
|
|
107
|
+
export const formatUserRefunds = (refunds) => refunds.map((refund) => [
|
|
63
108
|
refund.paymentId, refund.outcome.replace('_', ' '),
|
|
64
109
|
refund.error ?? formatMinor(refund.amountMinor, refund.currency), refund.refundIds.join(','),
|
|
65
110
|
].join('\t'));
|
|
66
|
-
export const
|
|
111
|
+
export const formatUserCancellation = (result) => [
|
|
67
112
|
`Subscription\t${result.subscriptionId} (${result.provider})`,
|
|
68
113
|
`Status\t${result.status}${result.cancelAtPeriodEnd ? ', cancels at period end' : ''}`,
|
|
69
114
|
`Access until\t${result.accessUntil ?? ''} (was ${result.accessUntilBefore ?? 'unknown'})`,
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
"minimumCliVersion": "0.1.20",
|
|
5
5
|
"entries": [
|
|
6
6
|
{
|
|
7
|
-
"repositoryCliVersion": "0.1.
|
|
7
|
+
"repositoryCliVersion": "0.1.316",
|
|
8
8
|
"manifest": {
|
|
9
9
|
"schemaVersion": 1,
|
|
10
|
-
"bundleVersion": "2.0.
|
|
10
|
+
"bundleVersion": "2.0.309",
|
|
11
11
|
"stepContractVersion": 3,
|
|
12
12
|
"contractHash": "d761e91d5ac6ff9e72c6d49c5bcd014270912473a66f99c49d726c65998085cf",
|
|
13
13
|
"managedFiles": [
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
47
|
"path": "docs/funnelsgrove/migrations/step-contract-v3.md",
|
|
48
|
-
"sha256": "
|
|
48
|
+
"sha256": "ec9f9ffa1b9ba4d91ea391ad22f8aacabcc959172ec49c5e1cd7ecd57b233c02"
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
51
|
"path": "docs/funnelsgrove/qa/analytics.md",
|
|
@@ -145,16 +145,16 @@
|
|
|
145
145
|
},
|
|
146
146
|
{
|
|
147
147
|
"path": "funnel-docs.config.json",
|
|
148
|
-
"sha256": "
|
|
148
|
+
"sha256": "a659665cdead3841cf23888bd46447b58f7c0c9eb6baff917f7235ac9b8d5d57"
|
|
149
149
|
}
|
|
150
150
|
]
|
|
151
151
|
}
|
|
152
152
|
},
|
|
153
153
|
{
|
|
154
|
-
"repositoryCliVersion": "0.1.
|
|
154
|
+
"repositoryCliVersion": "0.1.315",
|
|
155
155
|
"manifest": {
|
|
156
156
|
"schemaVersion": 1,
|
|
157
|
-
"bundleVersion": "2.0.
|
|
157
|
+
"bundleVersion": "2.0.308",
|
|
158
158
|
"stepContractVersion": 3,
|
|
159
159
|
"contractHash": "d761e91d5ac6ff9e72c6d49c5bcd014270912473a66f99c49d726c65998085cf",
|
|
160
160
|
"managedFiles": [
|
|
@@ -192,7 +192,7 @@
|
|
|
192
192
|
},
|
|
193
193
|
{
|
|
194
194
|
"path": "docs/funnelsgrove/migrations/step-contract-v3.md",
|
|
195
|
-
"sha256": "
|
|
195
|
+
"sha256": "c2b60e841dc59ee11dcfb6483b6b86433def59eb63019c4628a8f6e180a61a09"
|
|
196
196
|
},
|
|
197
197
|
{
|
|
198
198
|
"path": "docs/funnelsgrove/qa/analytics.md",
|
|
@@ -292,16 +292,16 @@
|
|
|
292
292
|
},
|
|
293
293
|
{
|
|
294
294
|
"path": "funnel-docs.config.json",
|
|
295
|
-
"sha256": "
|
|
295
|
+
"sha256": "98d0552496075be689cd94d7f9ccd48db77d82e693da77ec2f922cf678607ee9"
|
|
296
296
|
}
|
|
297
297
|
]
|
|
298
298
|
}
|
|
299
299
|
},
|
|
300
300
|
{
|
|
301
|
-
"repositoryCliVersion": "0.1.
|
|
301
|
+
"repositoryCliVersion": "0.1.314",
|
|
302
302
|
"manifest": {
|
|
303
303
|
"schemaVersion": 1,
|
|
304
|
-
"bundleVersion": "2.0.
|
|
304
|
+
"bundleVersion": "2.0.307",
|
|
305
305
|
"stepContractVersion": 3,
|
|
306
306
|
"contractHash": "d761e91d5ac6ff9e72c6d49c5bcd014270912473a66f99c49d726c65998085cf",
|
|
307
307
|
"managedFiles": [
|
|
@@ -339,7 +339,7 @@
|
|
|
339
339
|
},
|
|
340
340
|
{
|
|
341
341
|
"path": "docs/funnelsgrove/migrations/step-contract-v3.md",
|
|
342
|
-
"sha256": "
|
|
342
|
+
"sha256": "e3f9cd10ba6c481c4cc6a0477abb9729da10131ed0bd64994eeba13fa50041e7"
|
|
343
343
|
},
|
|
344
344
|
{
|
|
345
345
|
"path": "docs/funnelsgrove/qa/analytics.md",
|
|
@@ -439,16 +439,16 @@
|
|
|
439
439
|
},
|
|
440
440
|
{
|
|
441
441
|
"path": "funnel-docs.config.json",
|
|
442
|
-
"sha256": "
|
|
442
|
+
"sha256": "1edc6ee4d8cd0146d5a3d42f8e411015f8c3147117232fb2f80a1169b672b7a0"
|
|
443
443
|
}
|
|
444
444
|
]
|
|
445
445
|
}
|
|
446
446
|
},
|
|
447
447
|
{
|
|
448
|
-
"repositoryCliVersion": "0.1.
|
|
448
|
+
"repositoryCliVersion": "0.1.313",
|
|
449
449
|
"manifest": {
|
|
450
450
|
"schemaVersion": 1,
|
|
451
|
-
"bundleVersion": "2.0.
|
|
451
|
+
"bundleVersion": "2.0.306",
|
|
452
452
|
"stepContractVersion": 3,
|
|
453
453
|
"contractHash": "d761e91d5ac6ff9e72c6d49c5bcd014270912473a66f99c49d726c65998085cf",
|
|
454
454
|
"managedFiles": [
|
|
@@ -486,7 +486,7 @@
|
|
|
486
486
|
},
|
|
487
487
|
{
|
|
488
488
|
"path": "docs/funnelsgrove/migrations/step-contract-v3.md",
|
|
489
|
-
"sha256": "
|
|
489
|
+
"sha256": "4d9fa0b4846945524ec6d45562a8ee7cc48c64ce8f73510024445106db9e26ce"
|
|
490
490
|
},
|
|
491
491
|
{
|
|
492
492
|
"path": "docs/funnelsgrove/qa/analytics.md",
|
|
@@ -586,16 +586,16 @@
|
|
|
586
586
|
},
|
|
587
587
|
{
|
|
588
588
|
"path": "funnel-docs.config.json",
|
|
589
|
-
"sha256": "
|
|
589
|
+
"sha256": "61802de19c7144122977a735d98141b175d04e7880fa342e35a80bda6dfe966f"
|
|
590
590
|
}
|
|
591
591
|
]
|
|
592
592
|
}
|
|
593
593
|
},
|
|
594
594
|
{
|
|
595
|
-
"repositoryCliVersion": "0.1.
|
|
595
|
+
"repositoryCliVersion": "0.1.312",
|
|
596
596
|
"manifest": {
|
|
597
597
|
"schemaVersion": 1,
|
|
598
|
-
"bundleVersion": "2.0.
|
|
598
|
+
"bundleVersion": "2.0.305",
|
|
599
599
|
"stepContractVersion": 3,
|
|
600
600
|
"contractHash": "d761e91d5ac6ff9e72c6d49c5bcd014270912473a66f99c49d726c65998085cf",
|
|
601
601
|
"managedFiles": [
|
|
@@ -633,7 +633,7 @@
|
|
|
633
633
|
},
|
|
634
634
|
{
|
|
635
635
|
"path": "docs/funnelsgrove/migrations/step-contract-v3.md",
|
|
636
|
-
"sha256": "
|
|
636
|
+
"sha256": "2d1f917a42f99071709e3849d66d5434291bf899798cbb1c0094791cbedd8d96"
|
|
637
637
|
},
|
|
638
638
|
{
|
|
639
639
|
"path": "docs/funnelsgrove/qa/analytics.md",
|
|
@@ -733,16 +733,16 @@
|
|
|
733
733
|
},
|
|
734
734
|
{
|
|
735
735
|
"path": "funnel-docs.config.json",
|
|
736
|
-
"sha256": "
|
|
736
|
+
"sha256": "d9d21cbd6ecd576f37222e4f5c75808683709410452c34f2779d56b1d44fe445"
|
|
737
737
|
}
|
|
738
738
|
]
|
|
739
739
|
}
|
|
740
740
|
},
|
|
741
741
|
{
|
|
742
|
-
"repositoryCliVersion": "0.1.
|
|
742
|
+
"repositoryCliVersion": "0.1.311",
|
|
743
743
|
"manifest": {
|
|
744
744
|
"schemaVersion": 1,
|
|
745
|
-
"bundleVersion": "2.0.
|
|
745
|
+
"bundleVersion": "2.0.304",
|
|
746
746
|
"stepContractVersion": 3,
|
|
747
747
|
"contractHash": "d761e91d5ac6ff9e72c6d49c5bcd014270912473a66f99c49d726c65998085cf",
|
|
748
748
|
"managedFiles": [
|
|
@@ -780,7 +780,7 @@
|
|
|
780
780
|
},
|
|
781
781
|
{
|
|
782
782
|
"path": "docs/funnelsgrove/migrations/step-contract-v3.md",
|
|
783
|
-
"sha256": "
|
|
783
|
+
"sha256": "95f7f9c522a98f16c42214f93747dc49d8f86b56ef5f56b202e29c7a2725d0f7"
|
|
784
784
|
},
|
|
785
785
|
{
|
|
786
786
|
"path": "docs/funnelsgrove/qa/analytics.md",
|
|
@@ -880,16 +880,16 @@
|
|
|
880
880
|
},
|
|
881
881
|
{
|
|
882
882
|
"path": "funnel-docs.config.json",
|
|
883
|
-
"sha256": "
|
|
883
|
+
"sha256": "7677b66b8377a90463e559ce5817818e8ab396c0f4192882bc77f2792ab8db47"
|
|
884
884
|
}
|
|
885
885
|
]
|
|
886
886
|
}
|
|
887
887
|
},
|
|
888
888
|
{
|
|
889
|
-
"repositoryCliVersion": "0.1.
|
|
889
|
+
"repositoryCliVersion": "0.1.310",
|
|
890
890
|
"manifest": {
|
|
891
891
|
"schemaVersion": 1,
|
|
892
|
-
"bundleVersion": "2.0.
|
|
892
|
+
"bundleVersion": "2.0.303",
|
|
893
893
|
"stepContractVersion": 3,
|
|
894
894
|
"contractHash": "d761e91d5ac6ff9e72c6d49c5bcd014270912473a66f99c49d726c65998085cf",
|
|
895
895
|
"managedFiles": [
|
|
@@ -927,7 +927,7 @@
|
|
|
927
927
|
},
|
|
928
928
|
{
|
|
929
929
|
"path": "docs/funnelsgrove/migrations/step-contract-v3.md",
|
|
930
|
-
"sha256": "
|
|
930
|
+
"sha256": "dfaf7e01284c3fed5334218cb24a3497acfb8a7a5dbc03a232d5f8a7ac468d6a"
|
|
931
931
|
},
|
|
932
932
|
{
|
|
933
933
|
"path": "docs/funnelsgrove/qa/analytics.md",
|
|
@@ -1027,7 +1027,7 @@
|
|
|
1027
1027
|
},
|
|
1028
1028
|
{
|
|
1029
1029
|
"path": "funnel-docs.config.json",
|
|
1030
|
-
"sha256": "
|
|
1030
|
+
"sha256": "4aae4fd505bfbfc8338354969920a08a46f64e0b1e843bdb20c9462d1327eef0"
|
|
1031
1031
|
}
|
|
1032
1032
|
]
|
|
1033
1033
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"bundleVersion": "2.0.
|
|
3
|
+
"bundleVersion": "2.0.309",
|
|
4
4
|
"stepContractVersion": 3,
|
|
5
5
|
"contractHash": "d761e91d5ac6ff9e72c6d49c5bcd014270912473a66f99c49d726c65998085cf",
|
|
6
6
|
"managedFiles": [
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
"path": "docs/funnelsgrove/migrations/step-contract-v3.md",
|
|
41
|
-
"sha256": "
|
|
41
|
+
"sha256": "ec9f9ffa1b9ba4d91ea391ad22f8aacabcc959172ec49c5e1cd7ecd57b233c02"
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
44
|
"path": "docs/funnelsgrove/qa/analytics.md",
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
140
|
"path": "funnel-docs.config.json",
|
|
141
|
-
"sha256": "
|
|
141
|
+
"sha256": "a659665cdead3841cf23888bd46447b58f7c0c9eb6baff917f7235ac9b8d5d57"
|
|
142
142
|
}
|
|
143
143
|
]
|
|
144
144
|
}
|
|
@@ -17,7 +17,7 @@ Supported read versions: `1`, `2`, `3`. Authoring and publish target version `3`
|
|
|
17
17
|
|
|
18
18
|
### Package release order
|
|
19
19
|
|
|
20
|
-
Release `@funnelsgrove/sdk` `0.4.0` first, then `@funnelsgrove/runtime` `0.40.0`, then `@funnelsgrove/analytics` `0.23.0`, then `@funnelsgrove/payments` `0.57.0`. The production deploy verifies the zero-traffic API candidate, publishes and verifies `@funnelsgrove/cli` `0.1.
|
|
20
|
+
Release `@funnelsgrove/sdk` `0.4.0` first, then `@funnelsgrove/runtime` `0.40.0`, then `@funnelsgrove/analytics` `0.23.0`, then `@funnelsgrove/payments` `0.57.0`. The production deploy verifies the zero-traffic API candidate, publishes and verifies `@funnelsgrove/cli` `0.1.316`, and only then promotes the candidate to production traffic. The serving API must never advertise an unpublished preferred CLI. Publishing packages and deploying production remain separately approved operational actions.
|
|
21
21
|
<!-- funnelsgrove:generated:end contract-v3/migration/step-contract-v3 -->
|
|
22
22
|
|
|
23
23
|
## Version-last policy
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"bundleVersion": "2.0.
|
|
3
|
+
"bundleVersion": "2.0.309",
|
|
4
4
|
"stepContractVersion": 3,
|
|
5
5
|
"contractHash": "d761e91d5ac6ff9e72c6d49c5bcd014270912473a66f99c49d726c65998085cf",
|
|
6
6
|
"managedFiles": [
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
"path": "docs/funnelsgrove/migrations/step-contract-v3.md",
|
|
41
|
-
"sha256": "
|
|
41
|
+
"sha256": "ec9f9ffa1b9ba4d91ea391ad22f8aacabcc959172ec49c5e1cd7ecd57b233c02"
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
44
|
"path": "docs/funnelsgrove/qa/analytics.md",
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
140
|
"path": "funnel-docs.config.json",
|
|
141
|
-
"sha256": "
|
|
141
|
+
"sha256": "a659665cdead3841cf23888bd46447b58f7c0c9eb6baff917f7235ac9b8d5d57"
|
|
142
142
|
}
|
|
143
143
|
]
|
|
144
144
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"sourceTreeHash": "
|
|
3
|
+
"sourceTreeHash": "0f06f46537d05d2af0ced343de96a011b4be326217de99d5f566ef90a1acea72",
|
|
4
4
|
"stepContractVersion": 3,
|
|
5
|
-
"docsBundleVersion": "2.0.
|
|
5
|
+
"docsBundleVersion": "2.0.309",
|
|
6
6
|
"files": [
|
|
7
7
|
{
|
|
8
8
|
"path": ".env.example",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
"path": ".funnelsgrove-docs.json",
|
|
19
|
-
"sha256": "
|
|
19
|
+
"sha256": "5cdb7695c1f8627755689377bc15b3a33bf15cd254a0a590f58a83136b5498d3",
|
|
20
20
|
"mode": "100644"
|
|
21
21
|
},
|
|
22
22
|
{
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
},
|
|
77
77
|
{
|
|
78
78
|
"path": "docs/funnelsgrove/migrations/step-contract-v3.md",
|
|
79
|
-
"sha256": "
|
|
79
|
+
"sha256": "ec9f9ffa1b9ba4d91ea391ad22f8aacabcc959172ec49c5e1cd7ecd57b233c02",
|
|
80
80
|
"mode": "100644"
|
|
81
81
|
},
|
|
82
82
|
{
|
|
@@ -206,7 +206,7 @@
|
|
|
206
206
|
},
|
|
207
207
|
{
|
|
208
208
|
"path": "funnel-docs.config.json",
|
|
209
|
-
"sha256": "
|
|
209
|
+
"sha256": "a659665cdead3841cf23888bd46447b58f7c0c9eb6baff917f7235ac9b8d5d57",
|
|
210
210
|
"mode": "100644"
|
|
211
211
|
},
|
|
212
212
|
{
|
|
@@ -17,7 +17,7 @@ Supported read versions: `1`, `2`, `3`. Authoring and publish target version `3`
|
|
|
17
17
|
|
|
18
18
|
### Package release order
|
|
19
19
|
|
|
20
|
-
Release `@funnelsgrove/sdk` `0.4.0` first, then `@funnelsgrove/runtime` `0.40.0`, then `@funnelsgrove/analytics` `0.23.0`, then `@funnelsgrove/payments` `0.57.0`. The production deploy verifies the zero-traffic API candidate, publishes and verifies `@funnelsgrove/cli` `0.1.
|
|
20
|
+
Release `@funnelsgrove/sdk` `0.4.0` first, then `@funnelsgrove/runtime` `0.40.0`, then `@funnelsgrove/analytics` `0.23.0`, then `@funnelsgrove/payments` `0.57.0`. The production deploy verifies the zero-traffic API candidate, publishes and verifies `@funnelsgrove/cli` `0.1.316`, and only then promotes the candidate to production traffic. The serving API must never advertise an unpublished preferred CLI. Publishing packages and deploying production remain separately approved operational actions.
|
|
21
21
|
<!-- funnelsgrove:generated:end contract-v3/migration/step-contract-v3 -->
|
|
22
22
|
|
|
23
23
|
## Version-last policy
|