@clocknext/sdk 0.14.0 → 0.16.0

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.
@@ -1,6 +1,8 @@
1
1
  import type { Transport } from "../http.js";
2
+ import { newKey } from "../idempotency.js";
2
3
  import type {
3
4
  AddMemberInput,
5
+ AdjustResult,
4
6
  ApiCustomer,
5
7
  CompositeAdjustInput,
6
8
  CreateCustomerInput,
@@ -14,19 +16,18 @@ import type {
14
16
  MemberProfile,
15
17
  OutcomeAdjustInput,
16
18
  OutcomeBalance,
19
+ RecurringGrantInput,
20
+ RecurringGrantResult,
17
21
  RemovedMember,
18
22
  UnitAdjustInput,
19
23
  UnitBalance,
20
24
  UpdateMemberInput,
21
- UsageList,
22
- WalletEntryInput,
23
- WalletList,
24
- WalletTransaction,
25
+ WalletAdjustInput,
25
26
  } from "../types.js";
26
27
 
27
28
  /**
28
29
  * The `customers` resource — full CRUD over the org's customers plus the
29
- * read-only insight endpoints (usage / balances / plan). Every call is scoped
30
+ * read-only insight endpoints (balances / plan). Every call is scoped
30
31
  * to the API key's organisation server-side; a customer in another org
31
32
  * resolves as a NotFoundError.
32
33
  */
@@ -94,6 +95,42 @@ export class Customers {
94
95
  return res.customer;
95
96
  }
96
97
 
98
+ /**
99
+ * Fetch one customer by YOUR id for them (its `externalId`), instead of
100
+ * ours. Same result as {@link get}. `GET /api/v1/customers/ext/:externalId`.
101
+ * An unknown external id is a `NotFoundError`; only active customers match.
102
+ */
103
+ async getByExternalId(externalId: string): Promise<ApiCustomer> {
104
+ const res = await this.transport.request<{ customer: ApiCustomer }>({
105
+ method: "GET",
106
+ path: `/api/v1/customers/ext/${encodeURIComponent(externalId)}`,
107
+ });
108
+ return res.customer;
109
+ }
110
+
111
+ /** Update a customer (partial), addressed by its `externalId`. Same body as
112
+ * {@link update}. `PATCH /api/v1/customers/ext/:externalId`. */
113
+ async updateByExternalId(
114
+ externalId: string,
115
+ input: CustomerProfileInput,
116
+ ): Promise<ApiCustomer> {
117
+ const res = await this.transport.request<{ customer: ApiCustomer }>({
118
+ method: "PATCH",
119
+ path: `/api/v1/customers/ext/${encodeURIComponent(externalId)}`,
120
+ body: input,
121
+ });
122
+ return res.customer;
123
+ }
124
+
125
+ /** {@link delete}, addressing the customer by its `externalId`.
126
+ * `DELETE /api/v1/customers/ext/:externalId`. */
127
+ async deleteByExternalId(externalId: string): Promise<void> {
128
+ await this.transport.request({
129
+ method: "DELETE",
130
+ path: `/api/v1/customers/ext/${encodeURIComponent(externalId)}`,
131
+ });
132
+ }
133
+
97
134
  /** Delete a customer (members, invitations, usage logs cascade). */
98
135
  async delete(id: string): Promise<void> {
99
136
  await this.transport.request({
@@ -102,21 +139,20 @@ export class Customers {
102
139
  });
103
140
  }
104
141
 
105
- /** A customer's usage history. `GET /api/v1/usage?customerId=`. */
106
- async usage(id: string, params: { limit?: number } = {}): Promise<UsageList> {
107
- const res = await this.transport.request<UsageList>({
142
+ /** Wallet / credit / outcome / unit balances. `GET …/:id/balances`. */
143
+ balances(id: string): Promise<CustomerBalances> {
144
+ return this.transport.request<CustomerBalances>({
108
145
  method: "GET",
109
- path: `/api/v1/usage`,
110
- query: { customerId: id, limit: params.limit },
146
+ path: `/api/v1/customers/${encodeURIComponent(id)}/balances`,
111
147
  });
112
- return { customer: res.customer, totals: res.totals, logs: res.logs };
113
148
  }
114
149
 
115
- /** Wallet / credit / outcome / unit balances. `GET …/:id/balances`. */
116
- balances(id: string): Promise<CustomerBalances> {
150
+ /** {@link balances}, addressing the customer by its `externalId`.
151
+ * `GET /api/v1/customers/ext/:externalId/balances`. */
152
+ balancesByExternalId(externalId: string): Promise<CustomerBalances> {
117
153
  return this.transport.request<CustomerBalances>({
118
154
  method: "GET",
119
- path: `/api/v1/customers/${encodeURIComponent(id)}/balances`,
155
+ path: `/api/v1/customers/ext/${encodeURIComponent(externalId)}/balances`,
120
156
  });
121
157
  }
122
158
 
@@ -183,6 +219,15 @@ export class Customers {
183
219
  });
184
220
  }
185
221
 
222
+ /** {@link plan}, addressing the customer by its `externalId`.
223
+ * `GET /api/v1/customers/ext/:externalId/plan`. */
224
+ planByExternalId(externalId: string): Promise<CustomerPlan> {
225
+ return this.transport.request<CustomerPlan>({
226
+ method: "GET",
227
+ path: `/api/v1/customers/ext/${encodeURIComponent(externalId)}/plan`,
228
+ });
229
+ }
230
+
186
231
  // --- Members -------------------------------------------------------------
187
232
 
188
233
  /** List a customer's members. `GET …/:id/members`. */
@@ -227,95 +272,169 @@ export class Customers {
227
272
  return res.member;
228
273
  }
229
274
 
230
- // --- Wallet & billing config --------------------------------------------
275
+ // --- Billing config -----------------------------------------------------
231
276
 
232
- /** The customer's wallet balance + transactions. `GET …/:id/wallet`. */
233
- wallet(id: string, params: { limit?: number } = {}): Promise<WalletList> {
234
- return this.transport.request<WalletList>({
235
- method: "GET",
236
- path: `/api/v1/customers/${encodeURIComponent(id)}/wallet`,
237
- query: { limit: params.limit },
238
- });
277
+ // Note: a customer's currency is set through `update(id, { currencyCode })`
278
+ // (PATCH …/:id) — there is no separate currency endpoint. Credit-type
279
+ // assignment has no public API.
280
+
281
+ // --- Balance adjustments -------------------------------------------------
282
+
283
+ /** Grant or claw back a credit balance. The credit must be a prepaid part of
284
+ * the customer's current plan. Pass `idempotencyKey` to make it safe to
285
+ * retry. `POST …/:id/credits/adjust`. */
286
+ adjustCredit(id: string, input: CreditAdjustInput): Promise<AdjustResult> {
287
+ return this.adjust(`/api/v1/customers/${encodeURIComponent(id)}/credits/adjust`, input);
288
+ }
289
+
290
+ /** Grant or claw back an outcome balance. The outcome must be a prepaid part
291
+ * of the customer's current plan. `POST …/:id/outcomes/adjust`. */
292
+ adjustOutcome(id: string, input: OutcomeAdjustInput): Promise<AdjustResult> {
293
+ return this.adjust(`/api/v1/customers/${encodeURIComponent(id)}/outcomes/adjust`, input);
294
+ }
295
+
296
+ /** Grant or claw back a unit balance. The unit must be a prepaid part of the
297
+ * customer's current plan. `POST …/:id/units/adjust`. */
298
+ adjustUnit(id: string, input: UnitAdjustInput): Promise<AdjustResult> {
299
+ return this.adjust(`/api/v1/customers/${encodeURIComponent(id)}/units/adjust`, input);
300
+ }
301
+
302
+ /** Grant or claw back slots on a customer's ADVANCE composite pool for the
303
+ * current billing cycle. `remaining` is the resulting slot count.
304
+ * `POST …/:id/composites/adjust`. */
305
+ adjustComposite(id: string, input: CompositeAdjustInput): Promise<AdjustResult> {
306
+ return this.adjust(`/api/v1/customers/${encodeURIComponent(id)}/composites/adjust`, input);
239
307
  }
240
308
 
241
309
  /**
242
- * Record a manual wallet entry (top-up or deduction). `amount` is always
243
- * positive; the sign comes from `type`. `POST …/:id/wallet`.
310
+ * Top up (positive `delta`) or draw down (negative `delta`) a customer's
311
+ * wallet, in USD — allowed whatever the customer's plan. `remaining` is the
312
+ * resulting USD balance. No `agentKey` — a customer has exactly one wallet.
313
+ * Pass `pool: "reserved"` to move the Reserved Wallet instead (then
314
+ * `remaining` is its balance). `POST …/:id/wallet/adjust`.
244
315
  */
245
- async addWalletEntry(
246
- id: string,
247
- input: WalletEntryInput,
248
- ): Promise<{ transaction: WalletTransaction; balanceUsd: number }> {
249
- return this.transport.request<{
250
- transaction: WalletTransaction;
251
- balanceUsd: number;
252
- }>({
253
- method: "POST",
254
- path: `/api/v1/customers/${encodeURIComponent(id)}/wallet`,
255
- body: input,
256
- });
316
+ adjustWallet(id: string, input: WalletAdjustInput): Promise<AdjustResult> {
317
+ return this.adjust(`/api/v1/customers/${encodeURIComponent(id)}/wallet/adjust`, input);
257
318
  }
258
319
 
259
- // Note: a customer's currency is set through `update(id, { currencyCode })`
260
- // (PATCH …/:id) — there is no separate currency endpoint. Credit-type
261
- // assignment has no public API.
320
+ /** {@link adjustCredit}, addressing the customer by its `externalId` (`id`
321
+ * here). `POST /api/v1/customers/ext/:externalId/credits/adjust`. */
322
+ adjustCreditByExternalId(id: string, input: CreditAdjustInput): Promise<AdjustResult> {
323
+ return this.adjust(`/api/v1/customers/ext/${encodeURIComponent(id)}/credits/adjust`, input);
324
+ }
262
325
 
263
- // --- Balance adjustments -------------------------------------------------
326
+ /** {@link adjustOutcome}, addressing the customer by its `externalId` (`id`
327
+ * here). `POST /api/v1/customers/ext/:externalId/outcomes/adjust`. */
328
+ adjustOutcomeByExternalId(id: string, input: OutcomeAdjustInput): Promise<AdjustResult> {
329
+ return this.adjust(`/api/v1/customers/ext/${encodeURIComponent(id)}/outcomes/adjust`, input);
330
+ }
264
331
 
265
- /** Grant or claw back a credit balance. Returns the remaining balance.
266
- * `POST …/:id/credits/adjust`. */
267
- async adjustCredit(
268
- id: string,
269
- input: CreditAdjustInput,
270
- ): Promise<{ remaining: number }> {
271
- const res = await this.transport.request<{ remaining: number }>({
272
- method: "POST",
273
- path: `/api/v1/customers/${encodeURIComponent(id)}/credits/adjust`,
274
- body: input,
275
- });
276
- return { remaining: res.remaining };
332
+ /** {@link adjustUnit}, addressing the customer by its `externalId` (`id`
333
+ * here). `POST /api/v1/customers/ext/:externalId/units/adjust`. */
334
+ adjustUnitByExternalId(id: string, input: UnitAdjustInput): Promise<AdjustResult> {
335
+ return this.adjust(`/api/v1/customers/ext/${encodeURIComponent(id)}/units/adjust`, input);
277
336
  }
278
337
 
279
- /** Grant or claw back an outcome balance. Returns the remaining balance. */
280
- async adjustOutcome(
281
- id: string,
282
- input: OutcomeAdjustInput,
283
- ): Promise<{ remaining: number }> {
284
- const res = await this.transport.request<{ remaining: number }>({
285
- method: "POST",
286
- path: `/api/v1/customers/${encodeURIComponent(id)}/outcomes/adjust`,
287
- body: input,
288
- });
289
- return { remaining: res.remaining };
338
+ /** {@link adjustComposite}, addressing the customer by its `externalId`
339
+ * (`id` here). `POST /api/v1/customers/ext/:externalId/composites/adjust`. */
340
+ adjustCompositeByExternalId(id: string, input: CompositeAdjustInput): Promise<AdjustResult> {
341
+ return this.adjust(`/api/v1/customers/ext/${encodeURIComponent(id)}/composites/adjust`, input);
290
342
  }
291
343
 
292
- /** Grant or claw back a unit balance. Returns the remaining balance. */
293
- async adjustUnit(
294
- id: string,
295
- input: UnitAdjustInput,
296
- ): Promise<{ remaining: number }> {
297
- const res = await this.transport.request<{ remaining: number }>({
344
+ /** {@link adjustWallet}, addressing the customer by its `externalId` (`id`
345
+ * here). `POST /api/v1/customers/ext/:externalId/wallet/adjust`. */
346
+ adjustWalletByExternalId(id: string, input: WalletAdjustInput): Promise<AdjustResult> {
347
+ return this.adjust(`/api/v1/customers/ext/${encodeURIComponent(id)}/wallet/adjust`, input);
348
+ }
349
+
350
+ /**
351
+ * The one body behind all ten adjust calls.
352
+ *
353
+ * Every adjustment carries an idempotency key — the caller's, or one
354
+ * generated here ONCE per call, exactly like signals. The server
355
+ * deduplicates on it, so a transient failure is safe to retry: the same key
356
+ * rides every attempt, and a retry of one that already landed comes back as
357
+ * `replayed: true` instead of adjusting again. The key is also copied onto
358
+ * any thrown error (`error.idempotencyKey`) so the caller can replay safely.
359
+ */
360
+ private async adjust(
361
+ path: string,
362
+ input:
363
+ | CreditAdjustInput
364
+ | OutcomeAdjustInput
365
+ | UnitAdjustInput
366
+ | CompositeAdjustInput
367
+ | WalletAdjustInput,
368
+ ): Promise<AdjustResult> {
369
+ const idempotencyKey = input.idempotencyKey ?? newKey();
370
+ const res = await this.transport.request<{
371
+ remaining: number;
372
+ replayed?: boolean;
373
+ adjustedAt?: string;
374
+ customerArchived?: boolean;
375
+ }>({
298
376
  method: "POST",
299
- path: `/api/v1/customers/${encodeURIComponent(id)}/units/adjust`,
300
- body: input,
377
+ path,
378
+ idempotencyKey,
379
+ body: { ...input, idempotencyKey },
380
+ retry: true,
301
381
  });
302
- return { remaining: res.remaining };
382
+
383
+ const result: AdjustResult = {
384
+ remaining: res.remaining,
385
+ replayed: res.replayed === true,
386
+ customerArchived: res.customerArchived === true,
387
+ };
388
+ if (result.replayed && typeof res.adjustedAt === "string") {
389
+ result.adjustedAt = res.adjustedAt;
390
+ }
391
+ return result;
303
392
  }
304
393
 
394
+ // --- Recurring grants ----------------------------------------------------
395
+
305
396
  /**
306
- * Grant or claw back slots on a customer's ADVANCE composite pool for the
307
- * current billing cycle. Returns the resulting `remaining` slot count.
308
- * `POST …/:id/composites/adjust`.
397
+ * Change how much a customer is granted every billing cycle (wallet top-up,
398
+ * credits, outcomes, units or a composite pool). Any increase or decrease
399
+ * applies to their current balance immediately. `PUT …/:id/recurring-grant`.
309
400
  */
310
- async adjustComposite(
401
+ async updateRecurringGrant(
311
402
  id: string,
312
- input: CompositeAdjustInput,
313
- ): Promise<{ remaining: number }> {
314
- const res = await this.transport.request<{ remaining: number }>({
315
- method: "POST",
316
- path: `/api/v1/customers/${encodeURIComponent(id)}/composites/adjust`,
403
+ input: RecurringGrantInput,
404
+ ): Promise<RecurringGrantResult> {
405
+ const res = await this.transport.request<
406
+ RecurringGrantResult & { updated: true }
407
+ >({
408
+ method: "PUT",
409
+ path: `/api/v1/customers/${encodeURIComponent(id)}/recurring-grant`,
410
+ body: input,
411
+ });
412
+ return toRecurringGrantResult(res);
413
+ }
414
+
415
+ /** {@link updateRecurringGrant}, addressing the customer by its
416
+ * `externalId`. `PUT /api/v1/customers/ext/:externalId/recurring-grant`. */
417
+ async updateRecurringGrantByExternalId(
418
+ externalId: string,
419
+ input: RecurringGrantInput,
420
+ ): Promise<RecurringGrantResult> {
421
+ const res = await this.transport.request<
422
+ RecurringGrantResult & { updated: true }
423
+ >({
424
+ method: "PUT",
425
+ path: `/api/v1/customers/ext/${encodeURIComponent(externalId)}/recurring-grant`,
317
426
  body: input,
318
427
  });
319
- return { remaining: res.remaining };
428
+ return toRecurringGrantResult(res);
320
429
  }
321
430
  }
431
+
432
+ /** Drop the response's `updated: true` marker, keeping `balanceUsd` only when
433
+ * the server sent it (wallet grants). */
434
+ function toRecurringGrantResult(res: RecurringGrantResult): RecurringGrantResult {
435
+ return {
436
+ kind: res.kind,
437
+ quantity: res.quantity,
438
+ ...(res.balanceUsd !== undefined ? { balanceUsd: res.balanceUsd } : {}),
439
+ };
440
+ }
@@ -39,7 +39,7 @@ export class Invoices {
39
39
  }
40
40
 
41
41
  /**
42
- * Mint a hosted pay link for an OPEN invoice. PAID/VOID invoices are rejected
42
+ * Mint a hosted pay link for an OPEN invoice. PAID/VOID/AUTO_CLOSED invoices are rejected
43
43
  * with a `ConflictError`. `GET /api/v1/invoices/:id/pay-link`.
44
44
  */
45
45
  payLink(id: string): Promise<PayLink> {
@@ -1,29 +1,23 @@
1
1
  import { activeQuery, filterByActive } from "../filters.js";
2
2
  import type { Transport } from "../http.js";
3
- import type {
4
- CreateOutcomeInput,
5
- Outcome,
6
- OutcomeDetail,
7
- UpdateOutcomeInput,
8
- } from "../types.js";
3
+ import type { CreateOutcomeInput, Outcome, OutcomeWithSteps, UpdateOutcomeInput } from "../types.js";
9
4
 
10
5
  /**
11
- * The `outcomes` resource — manage the org's outcome *types* (the catalog). An
12
- * outcome is a multi-step workflow. `/api/v1/outcomes`.
6
+ * The `outcomes` resource — multi-step deliverables billed per completed
7
+ * outcome. `/api/v1/outcomes`.
13
8
  *
14
- * Two distinct kinds of agent key, in separate namespaces, each unique per
15
- * organisation — do not conflate them:
16
- * - the OUTCOME's own `agentKey`, REQUIRED on `create()` and `update()`, which
17
- * names the outcome itself in the catalogue;
18
- * - each STEP's `agentKey`, which is what you pass to `signals.outcome()` to
19
- * report usage and to the outcome-adjust API.
9
+ * `list()` and every write return the outcome without its steps; `get()`
10
+ * returns it with every step in full (ids included). Each new step is priced
11
+ * from its own `modelBundle`, on the server.
12
+ *
13
+ * There is no `delete()` — the API never deletes catalogue items. Retire an
14
+ * outcome with `archive()`; `unarchive()` brings it back unchanged.
20
15
  */
21
16
  export class Outcomes {
22
17
  constructor(private readonly transport: Transport) {}
23
18
 
24
- /** List outcome types. Pass `{ active: true }` to only return active ones.
25
- * `{ active: false }` returns only archived ones; omitting it returns
26
- * both. See `activeQuery` — the API itself only supports `true`. */
19
+ /** List them. `{ active: true }` returns only active ones, `{ active: false }`
20
+ * only archived ones; omit it for both. */
27
21
  async list(params: { active?: boolean } = {}): Promise<Outcome[]> {
28
22
  const res = await this.transport.request<{ outcomes: Outcome[] }>({
29
23
  method: "GET",
@@ -33,28 +27,31 @@ export class Outcomes {
33
27
  return filterByActive(res.outcomes, params.active);
34
28
  }
35
29
 
36
- /** Create an outcome type with its steps. `POST /api/v1/outcomes`. */
37
- async create(input: CreateOutcomeInput): Promise<Outcome> {
38
- const res = await this.transport.request<{ outcome: Outcome }>({
39
- method: "POST",
40
- path: "/api/v1/outcomes",
41
- body: input,
30
+ /** One outcome's configuration, with every step in full. */
31
+ async get(id: string): Promise<OutcomeWithSteps> {
32
+ const res = await this.transport.request<{ outcome: OutcomeWithSteps }>({
33
+ method: "GET",
34
+ path: `/api/v1/outcomes/${encodeURIComponent(id)}`,
42
35
  });
43
36
  return res.outcome;
44
37
  }
45
38
 
46
- /** Fetch one outcome type with steps + stats. `GET /api/v1/outcomes/:id`. */
47
- async get(id: string): Promise<OutcomeDetail> {
48
- const res = await this.transport.request<{ outcome: OutcomeDetail }>({
49
- method: "GET",
50
- path: `/api/v1/outcomes/${encodeURIComponent(id)}`,
39
+ /** Create one. It starts active. Not retried on failure — a retry could
40
+ * create a second row. */
41
+ async create(input: CreateOutcomeInput): Promise<Outcome> {
42
+ const res = await this.transport.request<{ created: true; outcome: Outcome }>({
43
+ method: "POST",
44
+ path: "/api/v1/outcomes",
45
+ body: input,
51
46
  });
52
47
  return res.outcome;
53
48
  }
54
49
 
55
- /** Update an outcome type's definition. `PATCH /api/v1/outcomes/:id`. */
50
+ /** PARTIAL update: only the fields you send change. Leave `steps` out to
51
+ * leave the steps alone; send it and it is the complete list — pass each
52
+ * existing step's `id` (or its same `agentKey`) to keep it. */
56
53
  async update(id: string, input: UpdateOutcomeInput): Promise<Outcome> {
57
- const res = await this.transport.request<{ outcome: Outcome }>({
54
+ const res = await this.transport.request<{ updated: true; outcome: Outcome }>({
58
55
  method: "PATCH",
59
56
  path: `/api/v1/outcomes/${encodeURIComponent(id)}`,
60
57
  body: input,
@@ -62,21 +59,23 @@ export class Outcomes {
62
59
  return res.outcome;
63
60
  }
64
61
 
65
- /** Activate or deactivate an outcome type; returns the updated outcome. */
66
- async setActive(id: string, isActive: boolean): Promise<Outcome> {
67
- const res = await this.transport.request<{ outcome: Outcome }>({
68
- method: "PATCH",
69
- path: `/api/v1/outcomes/${encodeURIComponent(id)}`,
70
- body: { isActive },
62
+ /** Retire it (`isActive` → false). Nothing is deleted. Safe to retry. */
63
+ async archive(id: string): Promise<Outcome> {
64
+ const res = await this.transport.request<{ archived: true; outcome: Outcome }>({
65
+ method: "POST",
66
+ path: `/api/v1/outcomes/${encodeURIComponent(id)}/archive`,
67
+ retry: true,
71
68
  });
72
69
  return res.outcome;
73
70
  }
74
71
 
75
- /** Delete an outcome type. */
76
- async delete(id: string): Promise<void> {
77
- await this.transport.request({
78
- method: "DELETE",
79
- path: `/api/v1/outcomes/${encodeURIComponent(id)}`,
72
+ /** Bring an archived one back (`isActive` → true). Safe to retry. */
73
+ async unarchive(id: string): Promise<Outcome> {
74
+ const res = await this.transport.request<{ unarchived: true; outcome: Outcome }>({
75
+ method: "POST",
76
+ path: `/api/v1/outcomes/${encodeURIComponent(id)}/unarchive`,
77
+ retry: true,
80
78
  });
79
+ return res.outcome;
81
80
  }
82
81
  }
@@ -1,16 +1,16 @@
1
1
  import { activeQuery, filterByActive } from "../filters.js";
2
2
  import type { Transport } from "../http.js";
3
- import type {
4
- CreatePlanInput,
5
- Plan,
6
- PlanDetail,
7
- UpdatePlanInput,
8
- } from "../types.js";
3
+ import type { CreatePlanInput, Plan, UpdatePlanInput } from "../types.js";
9
4
 
10
5
  /**
11
- * The `plans` resource — define and manage the billing plans customers
12
- * subscribe to. Each plan is a set of entitlement components (wallet / credit /
13
- * outcome / unit / flat) billed on a cycle. `/api/v1/plans`.
6
+ * The `plans` resource — the billing plans customers subscribe to. Each plan
7
+ * is a set of `entitlements` (wallet / credit / outcome / unit / composite /
8
+ * flat lines) billed on a cycle. `/api/v1/plans`.
9
+ *
10
+ * A plan's currency is the workspace's primary currency; it can't be chosen.
11
+ * There is no `delete()` — the API never deletes plans, so customers already
12
+ * on one keep what they bought. Retire a plan with `archive()`; `unarchive()`
13
+ * makes it purchasable again.
14
14
  */
15
15
  export class Plans {
16
16
  constructor(private readonly transport: Transport) {}
@@ -27,28 +27,31 @@ export class Plans {
27
27
  return filterByActive(res.plans, params.active);
28
28
  }
29
29
 
30
- /** Create a plan. `POST /api/v1/plans`. */
31
- async create(input: CreatePlanInput): Promise<Plan> {
30
+ /** One plan's configuration. */
31
+ async get(id: string): Promise<Plan> {
32
32
  const res = await this.transport.request<{ plan: Plan }>({
33
- method: "POST",
34
- path: "/api/v1/plans",
35
- body: input,
33
+ method: "GET",
34
+ path: `/api/v1/plans/${encodeURIComponent(id)}`,
36
35
  });
37
36
  return res.plan;
38
37
  }
39
38
 
40
- /** Fetch one plan with aggregate subscriber/invoice stats. `GET /api/v1/plans/:id`. */
41
- async get(id: string): Promise<PlanDetail> {
42
- const res = await this.transport.request<{ plan: PlanDetail }>({
43
- method: "GET",
44
- path: `/api/v1/plans/${encodeURIComponent(id)}`,
39
+ /** Create a plan. It starts active. Not retried on failure — a retry could
40
+ * create a second plan. */
41
+ async create(input: CreatePlanInput): Promise<Plan> {
42
+ const res = await this.transport.request<{ created: true; plan: Plan }>({
43
+ method: "POST",
44
+ path: "/api/v1/plans",
45
+ body: input,
45
46
  });
46
47
  return res.plan;
47
48
  }
48
49
 
49
- /** Replace a plan's definition (components + settings). `PATCH /api/v1/plans/:id`. */
50
+ /** PARTIAL update: only the fields you send change. `entitlements`, when
51
+ * sent, is the complete new list — the list `get()` returned can be sent
52
+ * back as it is. Changes apply to new purchases only. */
50
53
  async update(id: string, input: UpdatePlanInput): Promise<Plan> {
51
- const res = await this.transport.request<{ plan: Plan }>({
54
+ const res = await this.transport.request<{ updated: true; plan: Plan }>({
52
55
  method: "PATCH",
53
56
  path: `/api/v1/plans/${encodeURIComponent(id)}`,
54
57
  body: input,
@@ -56,21 +59,24 @@ export class Plans {
56
59
  return res.plan;
57
60
  }
58
61
 
59
- /** Activate or deactivate a plan without rewriting it. */
60
- async setActive(id: string, isActive: boolean): Promise<Plan> {
61
- const res = await this.transport.request<{ plan: Plan }>({
62
- method: "PATCH",
63
- path: `/api/v1/plans/${encodeURIComponent(id)}`,
64
- body: { isActive },
62
+ /** Retire a plan (`isActive` → false): it can't be bought any more; customers
63
+ * already on it are unaffected. Safe to retry. */
64
+ async archive(id: string): Promise<Plan> {
65
+ const res = await this.transport.request<{ archived: true; plan: Plan }>({
66
+ method: "POST",
67
+ path: `/api/v1/plans/${encodeURIComponent(id)}/archive`,
68
+ retry: true,
65
69
  });
66
70
  return res.plan;
67
71
  }
68
72
 
69
- /** Delete a plan. Refused once any customer has purchased it. */
70
- async delete(id: string): Promise<void> {
71
- await this.transport.request({
72
- method: "DELETE",
73
- path: `/api/v1/plans/${encodeURIComponent(id)}`,
73
+ /** Make an archived plan purchasable again (`isActive` → true). Safe to retry. */
74
+ async unarchive(id: string): Promise<Plan> {
75
+ const res = await this.transport.request<{ unarchived: true; plan: Plan }>({
76
+ method: "POST",
77
+ path: `/api/v1/plans/${encodeURIComponent(id)}/unarchive`,
78
+ retry: true,
74
79
  });
80
+ return res.plan;
75
81
  }
76
82
  }