@clocknext/sdk 0.17.0 → 0.19.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clocknext/sdk",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "Official ClockNext SDK — record usage signals and manage plans, purchases, invoices, customers, and billing from your backend.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/client.ts CHANGED
@@ -12,6 +12,7 @@ import { Portal } from "./resources/portal.js";
12
12
  import { Purchases } from "./resources/purchases.js";
13
13
  import { Signals } from "./resources/signals.js";
14
14
  import { Units } from "./resources/units.js";
15
+ import { Taxes } from "./resources/taxes.js";
15
16
  import type { ClockNextConfig, ResolvedConfig } from "./types.js";
16
17
 
17
18
  /**
@@ -46,6 +47,8 @@ export class ClockNext {
46
47
  readonly units: Units;
47
48
  /** Composites: list, get, create, update, archive, unarchive (no delete). */
48
49
  readonly composites: Composites;
50
+ /** Tax rates by country: list, get, create, update (archive in the dashboard). */
51
+ readonly taxes: Taxes;
49
52
  /** Subscribe customers to plans (purchases). */
50
53
  readonly purchases: Purchases;
51
54
  /** Read invoices and mint hosted pay links. */
@@ -76,6 +79,7 @@ export class ClockNext {
76
79
  this.outcomes = new Outcomes(transport);
77
80
  this.units = new Units(transport);
78
81
  this.composites = new Composites(transport);
82
+ this.taxes = new Taxes(transport);
79
83
  this.purchases = new Purchases(transport);
80
84
  this.invoices = new Invoices(transport);
81
85
  this.payments = new Payments(transport);
package/src/index.ts CHANGED
@@ -56,7 +56,6 @@ export type {
56
56
  CompositeCompletionInput,
57
57
  CompositeCompletionResult,
58
58
  SerializedUnitUsage,
59
- UsageLogPeriod,
60
59
  UsageLogListParams,
61
60
  UsageLogAppliedRule,
62
61
  UsageLogOutcomeAttribution,
@@ -69,6 +68,10 @@ export type {
69
68
  CreateCustomerInput,
70
69
  CustomerList,
71
70
  CustomerBalances,
71
+ WalletLedgerPage,
72
+ WalletLedgerParams,
73
+ WalletTransaction,
74
+ WalletTransactionSource,
72
75
  CreditBalance,
73
76
  OutcomeBalance,
74
77
  UnitBalance,
@@ -114,6 +117,11 @@ export type {
114
117
  Unit,
115
118
  CreateUnitInput,
116
119
  UpdateUnitInput,
120
+ // taxes
121
+ TaxBehavior,
122
+ Tax,
123
+ CreateTaxInput,
124
+ UpdateTaxInput,
117
125
  CompositeItem,
118
126
  Composite,
119
127
  CreateCompositeInput,
@@ -23,6 +23,9 @@ import type {
23
23
  UnitBalance,
24
24
  UpdateMemberInput,
25
25
  WalletAdjustInput,
26
+ WalletLedgerPage,
27
+ WalletLedgerParams,
28
+ WalletTransaction,
26
29
  } from "../types.js";
27
30
 
28
31
  /**
@@ -147,6 +150,57 @@ export class Customers {
147
150
  });
148
151
  }
149
152
 
153
+ /**
154
+ * One page of the customer's wallet ledger — every movement of wallet
155
+ * money, newest first — plus both wallet balances. Cursor-paginated: pass
156
+ * back `nextCursor` as `cursor`. `GET /api/v1/customers/:id/wallet`.
157
+ */
158
+ wallet(id: string, params: WalletLedgerParams = {}): Promise<WalletLedgerPage> {
159
+ return this.walletPage(`/api/v1/customers/${encodeURIComponent(id)}/wallet`, params);
160
+ }
161
+
162
+ /** {@link wallet}, addressing the customer by its `externalId`.
163
+ * `GET /api/v1/customers/ext/:externalId/wallet`. */
164
+ walletByExternalId(
165
+ externalId: string,
166
+ params: WalletLedgerParams = {},
167
+ ): Promise<WalletLedgerPage> {
168
+ return this.walletPage(
169
+ `/api/v1/customers/ext/${encodeURIComponent(externalId)}/wallet`,
170
+ params,
171
+ );
172
+ }
173
+
174
+ /**
175
+ * Async iterator over the customer's WHOLE wallet ledger, newest first,
176
+ * following the cursor. `params.cursor` is ignored.
177
+ * `for await (const t of cnk.customers.iterateWallet(id)) { … }`
178
+ */
179
+ async *iterateWallet(
180
+ id: string,
181
+ params: Omit<WalletLedgerParams, "cursor"> = {},
182
+ ): AsyncGenerator<WalletTransaction> {
183
+ let cursor: string | undefined;
184
+ do {
185
+ const page = await this.wallet(id, { ...params, cursor });
186
+ for (const t of page.transactions) yield t;
187
+ cursor = page.nextCursor ?? undefined;
188
+ } while (cursor);
189
+ }
190
+
191
+ private walletPage(path: string, params: WalletLedgerParams): Promise<WalletLedgerPage> {
192
+ return this.transport.request<WalletLedgerPage>({
193
+ method: "GET",
194
+ path,
195
+ query: {
196
+ limit: params.limit,
197
+ cursor: params.cursor,
198
+ from: params.from,
199
+ to: params.to,
200
+ },
201
+ });
202
+ }
203
+
150
204
  /** {@link balances}, addressing the customer by its `externalId`.
151
205
  * `GET /api/v1/customers/ext/:externalId/balances`. */
152
206
  balancesByExternalId(externalId: string): Promise<CustomerBalances> {
@@ -286,6 +286,7 @@ export class Signals {
286
286
  ...customerRefToWire(input),
287
287
  runId: input.runId,
288
288
  ...(input.member ? { member: input.member } : {}),
289
+ ...(input.custom ? { custom: input.custom } : {}),
289
290
  idempotencyKey,
290
291
  },
291
292
  retry: true,
@@ -328,6 +329,7 @@ export class Signals {
328
329
  ...(input.composite
329
330
  ? { composite: { [input.composite.ref]: input.composite.value } }
330
331
  : {}),
332
+ ...(input.custom ? { custom: input.custom } : {}),
331
333
  // Assigned HERE, before the send, so the SAME key rides every
332
334
  // transport-level retry below. Without it a single 500 followed by a
333
335
  // success is TWO un-keyed reports, and the server dedups on the key —
@@ -391,6 +393,7 @@ export class Signals {
391
393
  body: {
392
394
  ...customerRefToWire(input),
393
395
  composite: { [input.composite.ref]: input.composite.value },
396
+ ...(input.custom ? { custom: input.custom } : {}),
394
397
  // Auto-assigned like the others. A retry here could not double-BILL
395
398
  // even without it — closing a closed occurrence is a server-side
396
399
  // no-op — but that safety comes from occurrence-level dedup, not from
@@ -436,7 +439,6 @@ export class Signals {
436
439
  customerId: params.customerId,
437
440
  limit: params.limit,
438
441
  cursor: params.cursor,
439
- period: params.period,
440
442
  from: params.from,
441
443
  to: params.to,
442
444
  },
@@ -0,0 +1,59 @@
1
+ import { activeQuery, filterByActive } from "../filters.js";
2
+ import type { Transport } from "../http.js";
3
+ import type { CreateTaxInput, Tax, UpdateTaxInput } from "../types.js";
4
+
5
+ /**
6
+ * The `taxes` resource — the tax rates charged to customers by billing
7
+ * country (the same rates as Settings → Taxes). `/api/v1/taxes`.
8
+ *
9
+ * Every active tax for a customer's country is applied to their invoices and
10
+ * portal purchases automatically. There is no `delete()`, `archive()` or
11
+ * `unarchive()` — archiving and restoring a tax is done in the dashboard.
12
+ */
13
+ export class Taxes {
14
+ constructor(private readonly transport: Transport) {}
15
+
16
+ /** List them, oldest first. `{ active: true }` returns only active ones,
17
+ * `{ active: false }` only archived ones; omit it for both. */
18
+ async list(params: { active?: boolean } = {}): Promise<Tax[]> {
19
+ const res = await this.transport.request<{ taxes: Tax[] }>({
20
+ method: "GET",
21
+ path: "/api/v1/taxes",
22
+ query: { active: activeQuery(params.active) },
23
+ });
24
+ return filterByActive(res.taxes, params.active);
25
+ }
26
+
27
+ /** One tax. */
28
+ async get(id: string): Promise<Tax> {
29
+ const res = await this.transport.request<{ tax: Tax }>({
30
+ method: "GET",
31
+ path: `/api/v1/taxes/${encodeURIComponent(id)}`,
32
+ });
33
+ return res.tax;
34
+ }
35
+
36
+ /** Create one. It starts active. Two active taxes can't share a name
37
+ * (ignoring case) in the same country — that's a `ConflictError`. Not
38
+ * retried on failure — a retry could create a second row. */
39
+ async create(input: CreateTaxInput): Promise<Tax> {
40
+ const res = await this.transport.request<{ created: true; tax: Tax }>({
41
+ method: "POST",
42
+ path: "/api/v1/taxes",
43
+ body: input,
44
+ });
45
+ return res.tax;
46
+ }
47
+
48
+ /** PARTIAL update: only the fields you send change. Archived taxes can be
49
+ * edited and stay archived. Only invoices issued afterwards use the new
50
+ * values. */
51
+ async update(id: string, input: UpdateTaxInput): Promise<Tax> {
52
+ const res = await this.transport.request<{ updated: true; tax: Tax }>({
53
+ method: "PATCH",
54
+ path: `/api/v1/taxes/${encodeURIComponent(id)}`,
55
+ body: input,
56
+ });
57
+ return res.tax;
58
+ }
59
+ }
package/src/types.ts CHANGED
@@ -189,6 +189,8 @@ export type OutcomeCompletionInput = CustomerRef & {
189
189
  runId: string;
190
190
  /** Optional member (email) this completion is attributed to. */
191
191
  member?: string;
192
+ /** Free-form metadata, stored as-is with this completion. Never priced. */
193
+ custom?: Record<string, unknown>;
192
194
  /** Idempotency key, auto-generated if omitted. Sending this completion twice
193
195
  * for the same run is already a safe no-op server-side, so this only
194
196
  * collapses a retry into one attempt rather than protecting against a
@@ -383,9 +385,6 @@ export interface TrackResult {
383
385
  // Usage log (mirrors GET /api/v1/signal/usage-log)
384
386
  // ---------------------------------------------------------------------------
385
387
 
386
- /** A preset time window, or `"custom"` for an explicit `from`/`to` range. */
387
- export type UsageLogPeriod = "all" | "today" | "7d" | "30d" | "90d" | "custom";
388
-
389
388
  export interface UsageLogListParams {
390
389
  customerId: string;
391
390
  /** Default 50, max 200. */
@@ -393,11 +392,11 @@ export interface UsageLogListParams {
393
392
  /** Pass back a previous page's `nextCursor` to fetch the next page. Omit for
394
393
  * the first page. */
395
394
  cursor?: string;
396
- /** Default `"all"`. Pair `"custom"` with `from`/`to`. */
397
- period?: UsageLogPeriod;
398
- /** `YYYY-MM-DD`, only used when `period` is `"custom"`. */
395
+ /** First day to include, `YYYY-MM-DD` (UTC). The whole day is included.
396
+ * Keep sending the same value while paging with `cursor`. */
399
397
  from?: string;
400
- /** `YYYY-MM-DD`, only used when `period` is `"custom"`. */
398
+ /** Last day to include, `YYYY-MM-DD` (UTC). The whole day is included.
399
+ * Must not be before `from`. */
401
400
  to?: string;
402
401
  }
403
402
 
@@ -426,13 +425,15 @@ export interface UsageLogOutcomeAttribution {
426
425
  * One row of a customer's usage history, merged from every meter kind
427
426
  * (`UsageLog`, `UnitUsageLog`, completed composite occurrences) into one
428
427
  * feed and sorted by time — the same rows the customer portal's own "Usage
429
- * logs" table shows. `model`, `credit`, `outcome`, `unit` and `composite` are
430
- * each `null` unless `kind` matches that group.
428
+ * logs" table shows. `credit`, `outcome`, `unit` and `composite` are each
429
+ * `null` unless `type` matches that group.
431
430
  */
432
431
  export interface UsageLogEntry {
433
432
  id: string;
434
433
  createdAt: string;
435
- kind: "credit" | "outcome" | "unit" | "wallet" | "composite";
434
+ /** Which meter this row is. `"outcome"` is one step of a run;
435
+ * `"outcome_completion"` is the finished run itself (one per run). */
436
+ type: "credit" | "outcome" | "outcome_completion" | "unit" | "wallet" | "composite";
436
437
  status: SignalStatus;
437
438
  typeName: string | null;
438
439
  member: string | null;
@@ -447,16 +448,33 @@ export interface UsageLogEntry {
447
448
  };
448
449
  /** Formatted USD strings (e.g. `"$0.0064"`). */
449
450
  cost: { providedCost: string; customerCost: string };
450
- /** Populated only when `kind === "credit"`. `amount` is a formatted credit
451
+ /** Populated only when `type === "credit"`. `amount` is a formatted credit
451
452
  * value, honouring the org's credit-decimals display setting. */
452
453
  credit: { amount: string; appliedRules: UsageLogAppliedRule[] } | null;
453
- /** Populated only when `kind === "outcome"`. */
454
+ /** Populated only when `type === "outcome"`. */
454
455
  outcome: UsageLogOutcomeAttribution | null;
455
- /** Populated only when `kind === "unit"`. `quantity` is formatted. */
456
+ /** Populated only when `type === "unit"`. `quantity` is formatted. */
456
457
  unit: { quantity: string } | null;
457
- /** Populated only when `kind === "composite"`. `amount` is a formatted USD
458
+ /** Populated only when `type === "composite"`. `amount` is a formatted USD
458
459
  * price — the composite's flat price, read live, not stored per row. */
459
460
  composite: { amount: string } | null;
461
+ /** Populated only when `type === "outcome_completion"`. `amount` is the
462
+ * price frozen when the run completed (your plan price, else the catalogue
463
+ * price). */
464
+ outcomeCompletion: {
465
+ runId: string | null;
466
+ amount: string;
467
+ runSignals: number;
468
+ } | null;
469
+ /** The composite that covers this row's billing, by name — the row's price
470
+ * is then included in that composite's own charge, not billed on top. Set
471
+ * on credit / outcome / unit / outcome_completion rows tagged into a
472
+ * composite on the customer's plan; `null` otherwise. */
473
+ coveredByComposite: string | null;
474
+ /** The `custom` object sent, as sent — on the signal itself, or on the
475
+ * `completeComposite()` / `completeOutcome()` call that closed a composite /
476
+ * outcome_completion row. `null` when none was sent. */
477
+ custom: Record<string, unknown> | null;
460
478
  }
461
479
 
462
480
  export interface UsageLogPage {
@@ -490,8 +508,6 @@ export interface ApiCustomer {
490
508
  taxId: string | null;
491
509
  currencyCode: string;
492
510
  creditAllowance: number;
493
- recurringTransactionEmail: boolean;
494
- autoRecurring: boolean;
495
511
  createdAt: string;
496
512
  updatedAt: string;
497
513
  }
@@ -522,10 +538,6 @@ export interface CustomerProfileInput {
522
538
  taxId?: string | null;
523
539
  /** ISO 4217 (3 letters); must be enabled for the org. */
524
540
  currencyCode?: string;
525
- /** Send the invoice, payment receipt and card-payment-failed emails for recurring invoices. Defaults to `true` on create. */
526
- recurringTransactionEmail?: boolean;
527
- /** Issue every recurring invoice as `AUTO_CLOSED` (applied as if paid, never payable). Defaults to `false` on create. */
528
- autoRecurring?: boolean;
529
541
  }
530
542
 
531
543
  /** `name` and `email` are required on create; every other field is optional. */
@@ -618,6 +630,83 @@ export interface CustomerBalances {
618
630
  composites: CompositeBalance[];
619
631
  }
620
632
 
633
+ /** What caused a wallet ledger row (`WalletTransaction.source`). */
634
+ export type WalletTransactionSource =
635
+ /** Wallet money from a new plan purchase. */
636
+ | "SUBSCRIPTION"
637
+ /** Wallet money from a plan renewal, or unused money removed at renewal. */
638
+ | "RENEWAL"
639
+ /** Changed in the ClockNext dashboard. */
640
+ | "MANUAL"
641
+ /** Changed with `customers.adjustWallet()`. */
642
+ | "API"
643
+ /** Top-up paid by the customer in the customer portal. */
644
+ | "CUSTOMER_PORTAL"
645
+ /** Charged by a signal; `usageLogId` links the signal. */
646
+ | "USAGE_LOG"
647
+ /** Usage cleared by a paid pay-as-you-go invoice. */
648
+ | "INVOICE_SETTLEMENT"
649
+ /** Settlement undone by a voided invoice. */
650
+ | "INVOICE_VOID";
651
+
652
+ /** One row of a customer's wallet ledger (`customers.wallet()`). */
653
+ export interface WalletTransaction {
654
+ id: string;
655
+ /** ISO 8601. */
656
+ createdAt: string;
657
+ /** `CREDIT` adds money, `DEBIT` takes it out. */
658
+ type: "CREDIT" | "DEBIT";
659
+ /** Which balance moved: the ordinary wallet or the Reserved Wallet. */
660
+ pool: "main" | "reserved";
661
+ source: WalletTransactionSource;
662
+ /** Always positive — `type` gives the direction. In `currencyCode`. */
663
+ amount: number;
664
+ /** The same amount in USD. */
665
+ amountUsd: number;
666
+ currencyCode: string;
667
+ /** 1 USD = this many units of `currencyCode`, at write time. */
668
+ currencyRateUsd: number;
669
+ /** Only text a person wrote: your own note on an `API` row, the admin's
670
+ * note on a `MANUAL` row. `null` on every row the system writes itself —
671
+ * `source` says what those are. */
672
+ notes: string | null;
673
+ /** On a usage charge for an entitlement: which kind. `null` otherwise. */
674
+ entitlementType: "CREDIT" | "OUTCOME" | "UNIT" | "COMPOSITE" | null;
675
+ /** Id of that credit / outcome / unit / composite. */
676
+ entitlementId: string | null;
677
+ /** Its name when the row was written. */
678
+ entitlementName: string | null;
679
+ /** On a `USAGE_LOG` row: the id of the matching row in
680
+ * `signals.usageLog()`. */
681
+ usageLogId: string | null;
682
+ /** On a `SUBSCRIPTION` / `RENEWAL` grant: the purchase it came from. */
683
+ purchaseId: string | null;
684
+ purchaseNumber: string | null;
685
+ }
686
+
687
+ /** One page of a customer's wallet ledger, newest first. */
688
+ export interface WalletLedgerPage {
689
+ /** Ordinary wallet balance, USD. May be negative. */
690
+ balanceUsd: number;
691
+ /** Reserved Wallet balance, USD; `null` when the workspace hasn't enabled
692
+ * it. */
693
+ reservedBalanceUsd: number | null;
694
+ transactions: WalletTransaction[];
695
+ /** Pass back as `cursor` for the next page; `null` on the last page. */
696
+ nextCursor: string | null;
697
+ }
698
+
699
+ export interface WalletLedgerParams {
700
+ /** Default 50, max 200. */
701
+ limit?: number;
702
+ /** A previous page's `nextCursor`. Omit for the first page. */
703
+ cursor?: string;
704
+ /** First day to include, `YYYY-MM-DD` (UTC). The whole day is included. */
705
+ from?: string;
706
+ /** Last day to include, `YYYY-MM-DD` (UTC). The whole day is included. */
707
+ to?: string;
708
+ }
709
+
621
710
  /** The customer's active plan (the `plan` field of `customers.plan()`);
622
711
  * `null` when the customer is unsubscribed. */
623
712
  export interface CustomerPlanDetails {
@@ -963,6 +1052,46 @@ export interface CreateUnitInput {
963
1052
  /** `units.update()` body — PARTIAL. `tiers`, when sent, is the complete list. */
964
1053
  export type UpdateUnitInput = Partial<CreateUnitInput>;
965
1054
 
1055
+ /** How a tax applies: `"inclusive"` — already inside the price;
1056
+ * `"exclusive"` — added on top. Lower case only. */
1057
+ export type TaxBehavior = "inclusive" | "exclusive";
1058
+
1059
+ /** A tax rate, charged to customers whose billing country matches. */
1060
+ export interface Tax {
1061
+ id: string;
1062
+ /** Shown on invoices. Unique among active taxes in the same country,
1063
+ * ignoring case. */
1064
+ name: string;
1065
+ /** A note for your team; not shown to customers. */
1066
+ description: string | null;
1067
+ /** 0–100. */
1068
+ percentage: number;
1069
+ /** ISO 3166-1 alpha-2, upper case (e.g. `"IN"`). */
1070
+ countryCode: string;
1071
+ behavior: TaxBehavior;
1072
+ /** `false` once archived in the dashboard (Settings → Taxes): it no longer
1073
+ * applies to new invoices. */
1074
+ isActive: boolean;
1075
+ createdAt: string;
1076
+ updatedAt: string;
1077
+ }
1078
+
1079
+ /** `taxes.create()` body. */
1080
+ export interface CreateTaxInput {
1081
+ /** 1–100 characters. */
1082
+ name: string;
1083
+ /** 0–100. */
1084
+ percentage: number;
1085
+ /** ISO 3166-1 alpha-2 (lower case is accepted). A country name is refused. */
1086
+ countryCode: string;
1087
+ behavior: TaxBehavior;
1088
+ /** Up to 500 characters. */
1089
+ description?: string | null;
1090
+ }
1091
+
1092
+ /** `taxes.update()` body — PARTIAL: only the fields you send change. */
1093
+ export type UpdateTaxInput = Partial<CreateTaxInput>;
1094
+
966
1095
  /** A catalogue item a composite wraps. */
967
1096
  export interface CompositeItem {
968
1097
  id: string;
@@ -1079,7 +1208,8 @@ export interface PurchaseDetail {
1079
1208
  // Invoices & Payments
1080
1209
  // ---------------------------------------------------------------------------
1081
1210
 
1082
- /** `AUTO_CLOSED` — a recurring invoice for a customer with `autoRecurring` on:
1211
+ /** `AUTO_CLOSED` — a recurring invoice of an organisation with "Auto close
1212
+ * invoices on recurring" on (dashboard: Settings → Organisation → Billing):
1083
1213
  * applied as if paid, but never payable. */
1084
1214
  export type InvoiceStatus = "OPEN" | "PAID" | "VOID" | "AUTO_CLOSED";
1085
1215
 
@@ -1488,6 +1618,8 @@ export type UnitInput = CustomerRef & {
1488
1618
  quantity?: number;
1489
1619
  /** Optional composite this unit report belongs to. See {@link CompositeTag}. */
1490
1620
  composite?: CompositeTag;
1621
+ /** Free-form metadata, stored as-is with this unit report. Never priced. */
1622
+ custom?: Record<string, unknown>;
1491
1623
  /**
1492
1624
  * Idempotency key for safe retries — a repeat carrying the same key returns
1493
1625
  * the original result instead of drawing the balance down twice.
@@ -1539,6 +1671,9 @@ export type CompositeCompletionInput = CustomerRef & {
1539
1671
  /** Which occurrence to close: the composite's `refId` and the correlation
1540
1672
  * value the signals were tagged with. */
1541
1673
  composite: CompositeTag;
1674
+ /** Free-form metadata, stored as-is on the occurrence this call closes.
1675
+ * Never priced. */
1676
+ custom?: Record<string, unknown>;
1542
1677
  /** Idempotency key, auto-generated if omitted. Closing twice is already a
1543
1678
  * safe no-op, so this only collapses retries into one recorded attempt
1544
1679
  * rather than protecting against a double charge. */