@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 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 customer (workspace admins only): look them up by email or funnel
247
- user ID, refund chosen payments and cancel their subscription:
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 customers lookup --project claimbee --email buyer@example.com --json # users, live subscriptions, payments with refund eligibility
251
- fgrove customers refund --project claimbee --email buyer@example.com --payment ch_123 --payment pay_456 --dry-run
252
- fgrove customers refund --project claimbee --email buyer@example.com --payment ch_123 --payment pay_456
253
- fgrove customers cancel --project claimbee --user-id 8f1c2d # at period end; --now cancels immediately without refunding the remainder
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
- customers. Stripe charges and subscriptions come live from Stripe; Solidgate
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 { cancelCustomerSubscription, formatCustomerCancellation, formatCustomerLookup, formatCustomerRefunds, formatRefundPreview, lookupCustomer, parseCustomerSelector, previewRefund, refundCustomerPayments, } from './customerCommands.js';
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 addCustomerOptions = (command) => command
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>', 'Customer email (exact match, case-insensitive)')
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 resolveCustomerCommandScope = async (options) => {
2296
- const selector = parseCustomerSelector(options);
2295
+ const resolveUserCommandScope = async (options) => {
2296
+ const selector = parseUserSelector(options);
2297
2297
  return { scope: await resolveProjectCommandScope(options), selector };
2298
2298
  };
2299
- const customersCommand = addExamples(program.command('customers').description('Look up a customer\'s subscriptions and payments, refund payments and cancel subscriptions (workspace admins)'), [
2300
- 'fgrove customers lookup --project claimbee --email buyer@example.com',
2301
- 'fgrove customers refund --project claimbee --email buyer@example.com --payment ch_123 --payment pay_456',
2302
- 'fgrove customers cancel --project claimbee --user-id 8f1c... --now',
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
- addCustomerOptions(customersCommand
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 customer\'s funnel users, live provider subscriptions and payments with refund eligibility'))
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 resolveCustomerCommandScope(options);
2309
- const lookup = await lookupCustomer(scope, selector);
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(formatCustomerLookup(lookup).map((line) => [line]));
2342
+ printValueRows(formatUserLookup(lookup).map((line) => [line]));
2315
2343
  });
2316
- addCustomerOptions(customersCommand
2344
+ addUserOptions(usersCommand
2317
2345
  .command('refund')
2318
- .description('Fully refund the remaining amount of each chosen payment (payment IDs from customers lookup)')
2319
- .requiredOption('--payment <id>', 'Payment ID from customers lookup; repeat for several', (value, previous = []) => [...previous, value])
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 resolveCustomerCommandScope(options);
2350
+ const { scope, selector } = await resolveUserCommandScope(options);
2323
2351
  if (options.dryRun) {
2324
- const preview = previewRefund(await lookupCustomer(scope, selector), options.payment);
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 refundCustomerPayments(scope, selector, options.payment);
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(formatCustomerRefunds(refunds).map((line) => [line]));
2364
+ printValueRows(formatUserRefunds(refunds).map((line) => [line]));
2337
2365
  });
2338
- addCustomerOptions(customersCommand
2366
+ addUserOptions(usersCommand
2339
2367
  .command('cancel')
2340
- .description('Cancel the customer\'s billing subscription at period end, or immediately with --now')
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 resolveCustomerCommandScope(options);
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 cancelCustomerSubscription(scope, selector, { subscriptionId: options.subscription, now: options.now });
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(formatCustomerCancellation(result).map((line) => [line]));
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 CustomerCommandScope = {
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 CustomerSelector = {
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 CustomerSubscription = {
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 CustomerPayment = {
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 CustomerLookup = {
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: CustomerSubscription[];
58
- payments: CustomerPayment[];
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 CustomerRefund = {
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 CustomerCancellation = {
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 parseCustomerSelector: (options: {
87
+ export declare const parseUserSelector: (options: {
88
88
  email?: string;
89
89
  userId?: string;
90
90
  env?: string;
91
- }) => CustomerSelector;
92
- export declare const lookupCustomer: (scope: CustomerCommandScope, selector: CustomerSelector) => Promise<CustomerLookup>;
93
- export declare const refundCustomerPayments: (scope: CustomerCommandScope, selector: CustomerSelector, paymentIds: string[]) => Promise<{
94
- refunds: CustomerRefund[];
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 cancelCustomerSubscription: (scope: CustomerCommandScope, selector: CustomerSelector, input: {
96
+ export declare const cancelUserSubscription: (scope: UserCommandScope, selector: UserSelector, input: {
97
97
  subscriptionId?: string;
98
98
  now?: boolean;
99
- }) => Promise<CustomerCancellation>;
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: CustomerLookup, paymentIds: string[]) => ({
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 formatCustomerLookup: (lookup: CustomerLookup) => string[];
118
- export declare const formatCustomerRefunds: (refunds: CustomerRefund[]) => string[];
119
- export declare const formatCustomerCancellation: (result: CustomerCancellation) => string[];
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 parseCustomerSelector = (options) => {
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 lookupCustomer = (scope, selector) => scope.callApi({
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 refundCustomerPayments = (scope, selector, paymentIds) => (scope.callApi({
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 cancelCustomerSubscription = (scope, selector, input) => scope.callApi({
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 customers lookup' };
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 formatCustomerLookup = (lookup) => {
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 formatCustomerRefunds = (refunds) => refunds.map((refund) => [
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 formatCustomerCancellation = (result) => [
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.315",
7
+ "repositoryCliVersion": "0.1.316",
8
8
  "manifest": {
9
9
  "schemaVersion": 1,
10
- "bundleVersion": "2.0.308",
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": "c2b60e841dc59ee11dcfb6483b6b86433def59eb63019c4628a8f6e180a61a09"
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": "98d0552496075be689cd94d7f9ccd48db77d82e693da77ec2f922cf678607ee9"
148
+ "sha256": "a659665cdead3841cf23888bd46447b58f7c0c9eb6baff917f7235ac9b8d5d57"
149
149
  }
150
150
  ]
151
151
  }
152
152
  },
153
153
  {
154
- "repositoryCliVersion": "0.1.314",
154
+ "repositoryCliVersion": "0.1.315",
155
155
  "manifest": {
156
156
  "schemaVersion": 1,
157
- "bundleVersion": "2.0.307",
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": "e3f9cd10ba6c481c4cc6a0477abb9729da10131ed0bd64994eeba13fa50041e7"
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": "1edc6ee4d8cd0146d5a3d42f8e411015f8c3147117232fb2f80a1169b672b7a0"
295
+ "sha256": "98d0552496075be689cd94d7f9ccd48db77d82e693da77ec2f922cf678607ee9"
296
296
  }
297
297
  ]
298
298
  }
299
299
  },
300
300
  {
301
- "repositoryCliVersion": "0.1.313",
301
+ "repositoryCliVersion": "0.1.314",
302
302
  "manifest": {
303
303
  "schemaVersion": 1,
304
- "bundleVersion": "2.0.306",
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": "4d9fa0b4846945524ec6d45562a8ee7cc48c64ce8f73510024445106db9e26ce"
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": "61802de19c7144122977a735d98141b175d04e7880fa342e35a80bda6dfe966f"
442
+ "sha256": "1edc6ee4d8cd0146d5a3d42f8e411015f8c3147117232fb2f80a1169b672b7a0"
443
443
  }
444
444
  ]
445
445
  }
446
446
  },
447
447
  {
448
- "repositoryCliVersion": "0.1.312",
448
+ "repositoryCliVersion": "0.1.313",
449
449
  "manifest": {
450
450
  "schemaVersion": 1,
451
- "bundleVersion": "2.0.305",
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": "2d1f917a42f99071709e3849d66d5434291bf899798cbb1c0094791cbedd8d96"
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": "d9d21cbd6ecd576f37222e4f5c75808683709410452c34f2779d56b1d44fe445"
589
+ "sha256": "61802de19c7144122977a735d98141b175d04e7880fa342e35a80bda6dfe966f"
590
590
  }
591
591
  ]
592
592
  }
593
593
  },
594
594
  {
595
- "repositoryCliVersion": "0.1.311",
595
+ "repositoryCliVersion": "0.1.312",
596
596
  "manifest": {
597
597
  "schemaVersion": 1,
598
- "bundleVersion": "2.0.304",
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": "95f7f9c522a98f16c42214f93747dc49d8f86b56ef5f56b202e29c7a2725d0f7"
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": "7677b66b8377a90463e559ce5817818e8ab396c0f4192882bc77f2792ab8db47"
736
+ "sha256": "d9d21cbd6ecd576f37222e4f5c75808683709410452c34f2779d56b1d44fe445"
737
737
  }
738
738
  ]
739
739
  }
740
740
  },
741
741
  {
742
- "repositoryCliVersion": "0.1.310",
742
+ "repositoryCliVersion": "0.1.311",
743
743
  "manifest": {
744
744
  "schemaVersion": 1,
745
- "bundleVersion": "2.0.303",
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": "dfaf7e01284c3fed5334218cb24a3497acfb8a7a5dbc03a232d5f8a7ac468d6a"
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": "4aae4fd505bfbfc8338354969920a08a46f64e0b1e843bdb20c9462d1327eef0"
883
+ "sha256": "7677b66b8377a90463e559ce5817818e8ab396c0f4192882bc77f2792ab8db47"
884
884
  }
885
885
  ]
886
886
  }
887
887
  },
888
888
  {
889
- "repositoryCliVersion": "0.1.309",
889
+ "repositoryCliVersion": "0.1.310",
890
890
  "manifest": {
891
891
  "schemaVersion": 1,
892
- "bundleVersion": "2.0.302",
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": "72d337fa007993d18455f281b9a784e2e15fb46e43038a2934ddfcc82d8c1d28"
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": "e381723d48a0772419a5f8d757de1bb8fc9d7b3fd7838ad6fcde8ce0031d77c3"
1030
+ "sha256": "4aae4fd505bfbfc8338354969920a08a46f64e0b1e843bdb20c9462d1327eef0"
1031
1031
  }
1032
1032
  ]
1033
1033
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/cli",
3
- "version": "0.1.315",
3
+ "version": "0.1.316",
4
4
  "description": "FunnelsGrove command-line tools for editing, syncing, and publishing funnels",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "bundleVersion": "2.0.308",
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": "c2b60e841dc59ee11dcfb6483b6b86433def59eb63019c4628a8f6e180a61a09"
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": "98d0552496075be689cd94d7f9ccd48db77d82e693da77ec2f922cf678607ee9"
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.315`, 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.
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.308",
3
+ "bundleVersion": "2.0.309",
4
4
  "contractSource": "funnelsgrove-repository://apps/funnel-runtime/contracts/step-contract-v2.json",
5
5
  "fullyGenerated": [
6
6
  ".funnelsgrove-docs.json",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "bundleVersion": "2.0.308",
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": "c2b60e841dc59ee11dcfb6483b6b86433def59eb63019c4628a8f6e180a61a09"
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": "98d0552496075be689cd94d7f9ccd48db77d82e693da77ec2f922cf678607ee9"
141
+ "sha256": "a659665cdead3841cf23888bd46447b58f7c0c9eb6baff917f7235ac9b8d5d57"
142
142
  }
143
143
  ]
144
144
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "sourceTreeHash": "578a4547e6d4d1d2127ba60ac824357712a86000f62c318d12db63725e7d78ba",
3
+ "sourceTreeHash": "0f06f46537d05d2af0ced343de96a011b4be326217de99d5f566ef90a1acea72",
4
4
  "stepContractVersion": 3,
5
- "docsBundleVersion": "2.0.308",
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": "a046775aa77fb7400960fbe3a4055f8dd3968c9edf8b9617344e4964f7dc5ecb",
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": "c2b60e841dc59ee11dcfb6483b6b86433def59eb63019c4628a8f6e180a61a09",
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": "98d0552496075be689cd94d7f9ccd48db77d82e693da77ec2f922cf678607ee9",
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.315`, 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.
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.308",
3
+ "bundleVersion": "2.0.309",
4
4
  "contractSource": "funnelsgrove-repository://apps/funnel-runtime/contracts/step-contract-v2.json",
5
5
  "fullyGenerated": [
6
6
  ".funnelsgrove-docs.json",