@clocknext/sdk 0.18.0 → 0.19.1

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.18.0",
3
+ "version": "0.19.1",
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/index.ts CHANGED
@@ -61,6 +61,9 @@ export type {
61
61
  UsageLogOutcomeAttribution,
62
62
  UsageLogEntry,
63
63
  UsageLogPage,
64
+ UsageLogLookupEntitlementType,
65
+ UsageLogLookupEntry,
66
+ UsageLogLookupResult,
64
67
  // customers
65
68
  CustomerRef,
66
69
  ApiCustomer,
@@ -68,6 +71,10 @@ export type {
68
71
  CreateCustomerInput,
69
72
  CustomerList,
70
73
  CustomerBalances,
74
+ WalletLedgerPage,
75
+ WalletLedgerParams,
76
+ WalletTransaction,
77
+ WalletTransactionSource,
71
78
  CreditBalance,
72
79
  OutcomeBalance,
73
80
  UnitBalance,
@@ -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> {
@@ -18,6 +18,7 @@ import type {
18
18
  UnitTrackResult,
19
19
  UsageLogListParams,
20
20
  UsageLogPage,
21
+ UsageLogLookupResult,
21
22
  WalletInput,
22
23
  } from "../types.js";
23
24
 
@@ -46,6 +47,7 @@ const UNIT_PATH = "/api/v1/signal/unit";
46
47
  const COMPOSITE_COMPLETE_PATH = "/api/v1/signal/composite/complete";
47
48
  const OUTCOME_COMPLETE_PATH = "/api/v1/signal/outcome/complete";
48
49
  const USAGE_LOG_PATH = "/api/v1/signal/usage-log";
50
+ const USAGE_LOG_LOOKUP_PATH = "/api/v1/signal/usage-log/lookup";
49
51
 
50
52
  /**
51
53
  * Builds the request body for a typed Signal.
@@ -286,6 +288,7 @@ export class Signals {
286
288
  ...customerRefToWire(input),
287
289
  runId: input.runId,
288
290
  ...(input.member ? { member: input.member } : {}),
291
+ ...(input.custom ? { custom: input.custom } : {}),
289
292
  idempotencyKey,
290
293
  },
291
294
  retry: true,
@@ -328,6 +331,7 @@ export class Signals {
328
331
  ...(input.composite
329
332
  ? { composite: { [input.composite.ref]: input.composite.value } }
330
333
  : {}),
334
+ ...(input.custom ? { custom: input.custom } : {}),
331
335
  // Assigned HERE, before the send, so the SAME key rides every
332
336
  // transport-level retry below. Without it a single 500 followed by a
333
337
  // success is TWO un-keyed reports, and the server dedups on the key —
@@ -391,6 +395,7 @@ export class Signals {
391
395
  body: {
392
396
  ...customerRefToWire(input),
393
397
  composite: { [input.composite.ref]: input.composite.value },
398
+ ...(input.custom ? { custom: input.custom } : {}),
394
399
  // Auto-assigned like the others. A retry here could not double-BILL
395
400
  // even without it — closing a closed occurrence is a server-side
396
401
  // no-op — but that safety comes from occurrence-level dedup, not from
@@ -441,4 +446,25 @@ export class Signals {
441
446
  },
442
447
  });
443
448
  }
449
+
450
+ /**
451
+ * Fetches up to 100 usage-log rows by id, in one call.
452
+ * `GET /api/v1/signal/usage-log/lookup`.
453
+ *
454
+ * Built for linking a wallet debit to its usage: pass the `usageLogId`s
455
+ * from `customers.wallet()` instead of paging through
456
+ * {@link usageLog}. Scoped to your org, not to one customer — every row
457
+ * carries its own `customerId`.
458
+ *
459
+ * Rows come back in the order of `ids`; duplicates are removed. Ids that
460
+ * match nothing in your org are listed in `notFound`. More than 100
461
+ * distinct ids is a 400.
462
+ */
463
+ lookupUsageLogs(ids: string[]): Promise<UsageLogLookupResult> {
464
+ return this.transport.request<UsageLogLookupResult>({
465
+ method: "GET",
466
+ path: USAGE_LOG_LOOKUP_PATH,
467
+ query: { ids: ids.join(",") },
468
+ });
469
+ }
444
470
  }
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
@@ -429,8 +431,9 @@ export interface UsageLogOutcomeAttribution {
429
431
  export interface UsageLogEntry {
430
432
  id: string;
431
433
  createdAt: string;
432
- /** Which meter this row is. */
433
- type: "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";
434
437
  status: SignalStatus;
435
438
  typeName: string | null;
436
439
  member: string | null;
@@ -455,8 +458,22 @@ export interface UsageLogEntry {
455
458
  /** Populated only when `type === "composite"`. `amount` is a formatted USD
456
459
  * price — the composite's flat price, read live, not stored per row. */
457
460
  composite: { amount: string } | null;
458
- /** The `custom` object sent on the signal, as sent. `null` when none was
459
- * sent, and always `null` for unit and composite rows. */
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. */
460
477
  custom: Record<string, unknown> | null;
461
478
  }
462
479
 
@@ -467,6 +484,83 @@ export interface UsageLogPage {
467
484
  nextCursor: string | null;
468
485
  }
469
486
 
487
+ // ---------------------------------------------------------------------------
488
+ // Usage log lookup (mirrors GET /api/v1/signal/usage-log/lookup)
489
+ // ---------------------------------------------------------------------------
490
+
491
+ /** What a {@link UsageLogLookupEntry} row is. `"outcome"` is one step of a
492
+ * run; `"outcome_completion"` and `"composite_completion"` are the finished
493
+ * runs themselves. */
494
+ export type UsageLogLookupEntitlementType =
495
+ | "credit"
496
+ | "outcome"
497
+ | "outcome_completion"
498
+ | "unit"
499
+ | "composite_completion"
500
+ | "wallet";
501
+
502
+ /**
503
+ * One row of `signals.lookupUsageLogs()`. Only the group that matches
504
+ * `entitlement.type` holds data; the others are `null`.
505
+ */
506
+ export interface UsageLogLookupEntry {
507
+ id: string;
508
+ /** The customer this row belongs to — a lookup can span customers. */
509
+ customerId: string;
510
+ /** For a finished run (`outcome_completion`, `composite_completion`), when
511
+ * it completed. */
512
+ createdAt: string;
513
+ status: SignalStatus;
514
+ /** The credit / outcome / unit / composite this row is for. A wallet row has
515
+ * `id: null` and `name: "Wallet"`. */
516
+ entitlement: { type: UsageLogLookupEntitlementType; id: string | null; name: string | null };
517
+ member: string | null;
518
+ model: { name: string | null; provider: string | null } | null;
519
+ usage: {
520
+ inputTokens: number;
521
+ outputTokens: number;
522
+ /** Cache-read tokens. */
523
+ cacheTokens: number;
524
+ /** Cache-write tokens, priced separately from cache reads. */
525
+ cacheWriteTokens: number;
526
+ };
527
+ /** Formatted USD strings. `customerCost` is what this row charged:
528
+ * `"$0.00"` on an outcome step (the run is billed at completion) and on any
529
+ * row a composite pays for. On a unit row it is what the wallet paid for
530
+ * that report (debits minus refunds), `"$0.00"` when the wallet paid
531
+ * nothing. */
532
+ cost: { providedCost: string; customerCost: string };
533
+ /** `entitlement.type === "credit"`. */
534
+ credit: { amount: string; appliedRules: UsageLogAppliedRule[] } | null;
535
+ /** `entitlement.type === "outcome"`. `runCompleted` is true once the run is
536
+ * finished — on every step row of that run. */
537
+ outcome: { step: string | null; stepName: string | null; runId: string | null; runCompleted: boolean } | null;
538
+ /** `entitlement.type === "outcome_completion"`. `amount` is the price frozen
539
+ * when the run completed. */
540
+ outcomeCompletion: { runId: string | null; amount: string } | null;
541
+ /** `entitlement.type === "unit"`. `quantity` is formatted. */
542
+ unit: { quantity: string } | null;
543
+ /** `entitlement.type === "composite_completion"`. `refId` is the
544
+ * composite's tag key, `runId` the tag value you sent. */
545
+ compositeCompletion: { refId: string | null; runId: string | null; amount: string; startedAt: string } | null;
546
+ /** `entitlement.type === "wallet"`. What the wallet paid, formatted USD —
547
+ * the same value as `cost.customerCost`. */
548
+ wallet: { amount: string } | null;
549
+ /** The composite that pays for this row — its id, its tag key (`refId`) and
550
+ * the tag value you sent (`runId`). `null` when the row is billed by
551
+ * itself. */
552
+ composite: { id: string | null; refId: string | null; runId: string | null } | null;
553
+ /** The `custom` object sent, as sent. `null` when none was sent. */
554
+ custom: Record<string, unknown> | null;
555
+ }
556
+
557
+ export interface UsageLogLookupResult {
558
+ /** The rows found, in the order of the ids you asked for. */
559
+ usageLogs: UsageLogLookupEntry[];
560
+ /** The ids that matched nothing in your org, in the order you asked. */
561
+ notFound: string[];
562
+ }
563
+
470
564
  // ---------------------------------------------------------------------------
471
565
  // Customers (mirrors src/lib/customers/api.ts)
472
566
  // ---------------------------------------------------------------------------
@@ -491,8 +585,6 @@ export interface ApiCustomer {
491
585
  taxId: string | null;
492
586
  currencyCode: string;
493
587
  creditAllowance: number;
494
- recurringTransactionEmail: boolean;
495
- autoRecurring: boolean;
496
588
  createdAt: string;
497
589
  updatedAt: string;
498
590
  }
@@ -523,10 +615,6 @@ export interface CustomerProfileInput {
523
615
  taxId?: string | null;
524
616
  /** ISO 4217 (3 letters); must be enabled for the org. */
525
617
  currencyCode?: string;
526
- /** Send the invoice, payment receipt and card-payment-failed emails for recurring invoices. Defaults to `true` on create. */
527
- recurringTransactionEmail?: boolean;
528
- /** Issue every recurring invoice as `AUTO_CLOSED` (applied as if paid, never payable). Defaults to `false` on create. */
529
- autoRecurring?: boolean;
530
618
  }
531
619
 
532
620
  /** `name` and `email` are required on create; every other field is optional. */
@@ -619,6 +707,83 @@ export interface CustomerBalances {
619
707
  composites: CompositeBalance[];
620
708
  }
621
709
 
710
+ /** What caused a wallet ledger row (`WalletTransaction.source`). */
711
+ export type WalletTransactionSource =
712
+ /** Wallet money from a new plan purchase. */
713
+ | "SUBSCRIPTION"
714
+ /** Wallet money from a plan renewal, or unused money removed at renewal. */
715
+ | "RENEWAL"
716
+ /** Changed in the ClockNext dashboard. */
717
+ | "MANUAL"
718
+ /** Changed with `customers.adjustWallet()`. */
719
+ | "API"
720
+ /** Top-up paid by the customer in the customer portal. */
721
+ | "CUSTOMER_PORTAL"
722
+ /** Charged by a signal; `usageLogId` links the signal. */
723
+ | "USAGE_LOG"
724
+ /** Usage cleared by a paid pay-as-you-go invoice. */
725
+ | "INVOICE_SETTLEMENT"
726
+ /** Settlement undone by a voided invoice. */
727
+ | "INVOICE_VOID";
728
+
729
+ /** One row of a customer's wallet ledger (`customers.wallet()`). */
730
+ export interface WalletTransaction {
731
+ id: string;
732
+ /** ISO 8601. */
733
+ createdAt: string;
734
+ /** `CREDIT` adds money, `DEBIT` takes it out. */
735
+ type: "CREDIT" | "DEBIT";
736
+ /** Which balance moved: the ordinary wallet or the Reserved Wallet. */
737
+ pool: "main" | "reserved";
738
+ source: WalletTransactionSource;
739
+ /** Always positive — `type` gives the direction. In `currencyCode`. */
740
+ amount: number;
741
+ /** The same amount in USD. */
742
+ amountUsd: number;
743
+ currencyCode: string;
744
+ /** 1 USD = this many units of `currencyCode`, at write time. */
745
+ currencyRateUsd: number;
746
+ /** Only text a person wrote: your own note on an `API` row, the admin's
747
+ * note on a `MANUAL` row. `null` on every row the system writes itself —
748
+ * `source` says what those are. */
749
+ notes: string | null;
750
+ /** On a usage charge for an entitlement: which kind. `null` otherwise. */
751
+ entitlementType: "CREDIT" | "OUTCOME" | "UNIT" | "COMPOSITE" | null;
752
+ /** Id of that credit / outcome / unit / composite. */
753
+ entitlementId: string | null;
754
+ /** Its name when the row was written. */
755
+ entitlementName: string | null;
756
+ /** On a `USAGE_LOG` row: the id of the matching row in
757
+ * `signals.usageLog()`. */
758
+ usageLogId: string | null;
759
+ /** On a `SUBSCRIPTION` / `RENEWAL` grant: the purchase it came from. */
760
+ purchaseId: string | null;
761
+ purchaseNumber: string | null;
762
+ }
763
+
764
+ /** One page of a customer's wallet ledger, newest first. */
765
+ export interface WalletLedgerPage {
766
+ /** Ordinary wallet balance, USD. May be negative. */
767
+ balanceUsd: number;
768
+ /** Reserved Wallet balance, USD; `null` when the workspace hasn't enabled
769
+ * it. */
770
+ reservedBalanceUsd: number | null;
771
+ transactions: WalletTransaction[];
772
+ /** Pass back as `cursor` for the next page; `null` on the last page. */
773
+ nextCursor: string | null;
774
+ }
775
+
776
+ export interface WalletLedgerParams {
777
+ /** Default 50, max 200. */
778
+ limit?: number;
779
+ /** A previous page's `nextCursor`. Omit for the first page. */
780
+ cursor?: string;
781
+ /** First day to include, `YYYY-MM-DD` (UTC). The whole day is included. */
782
+ from?: string;
783
+ /** Last day to include, `YYYY-MM-DD` (UTC). The whole day is included. */
784
+ to?: string;
785
+ }
786
+
622
787
  /** The customer's active plan (the `plan` field of `customers.plan()`);
623
788
  * `null` when the customer is unsubscribed. */
624
789
  export interface CustomerPlanDetails {
@@ -1120,7 +1285,8 @@ export interface PurchaseDetail {
1120
1285
  // Invoices & Payments
1121
1286
  // ---------------------------------------------------------------------------
1122
1287
 
1123
- /** `AUTO_CLOSED` — a recurring invoice for a customer with `autoRecurring` on:
1288
+ /** `AUTO_CLOSED` — a recurring invoice of an organisation with "Auto close
1289
+ * invoices on recurring" on (dashboard: Settings → Organisation → Billing):
1124
1290
  * applied as if paid, but never payable. */
1125
1291
  export type InvoiceStatus = "OPEN" | "PAID" | "VOID" | "AUTO_CLOSED";
1126
1292
 
@@ -1529,6 +1695,8 @@ export type UnitInput = CustomerRef & {
1529
1695
  quantity?: number;
1530
1696
  /** Optional composite this unit report belongs to. See {@link CompositeTag}. */
1531
1697
  composite?: CompositeTag;
1698
+ /** Free-form metadata, stored as-is with this unit report. Never priced. */
1699
+ custom?: Record<string, unknown>;
1532
1700
  /**
1533
1701
  * Idempotency key for safe retries — a repeat carrying the same key returns
1534
1702
  * the original result instead of drawing the balance down twice.
@@ -1580,6 +1748,9 @@ export type CompositeCompletionInput = CustomerRef & {
1580
1748
  /** Which occurrence to close: the composite's `refId` and the correlation
1581
1749
  * value the signals were tagged with. */
1582
1750
  composite: CompositeTag;
1751
+ /** Free-form metadata, stored as-is on the occurrence this call closes.
1752
+ * Never priced. */
1753
+ custom?: Record<string, unknown>;
1583
1754
  /** Idempotency key, auto-generated if omitted. Closing twice is already a
1584
1755
  * safe no-op, so this only collapses retries into one recorded attempt
1585
1756
  * rather than protecting against a double charge. */