@chatarmin/os 1.5.2 → 1.5.4

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/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@chatarmin/os` are documented here. The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
4
+
5
+ ## Unreleased
6
+
7
+ ### Added
8
+
9
+ - **`billing.getDashboardContext({ externalOrgId })`** — Single-call billing/usage context for a tenant: `companyId`, `productLinkId`, ordered feature rows with `remaining`, `canUse`, optional Stripe `billing`, and `billingDegraded` when subscription item linkage is missing. See SDK README “Dashboard context migration”.
10
+ - **`features.listForProduct(input?)`** — Feature definition catalog for the API key’s product (no `companyId`). Documented in SDK README.
11
+
12
+ ### Migration
13
+
14
+ - For dashboards, prefer `billing.getDashboardContext` + `features.listForProduct` instead of chaining `companies.getByProductLink` + `features.listAccess` and hardcoding `featureCode` lists. Older methods remain available.
15
+
16
+ Product PRD (repo): `docs/prds/2026-03-23-product-sdk-dashboard-context.md`.
package/README.md CHANGED
@@ -31,6 +31,7 @@
31
31
  - [Links](#links)
32
32
  - [Onboarding](#onboarding)
33
33
  - [Common Patterns](#common-patterns)
34
+ - [Changelog](#changelog)
34
35
  - [TypeScript Support](#typescript-support)
35
36
  - [Configuration](#configuration)
36
37
  - [Error Handling](#error-handling)
@@ -275,6 +276,17 @@ for (const f of features) {
275
276
  }
276
277
  ```
277
278
 
279
+ #### `features.listForProduct(input?)`
280
+
281
+ List **feature definitions** attached to your product (the API key’s product). No company id—use for catalog UI, empty states, or multi-feature shells. Inactive features are omitted unless you pass `{ includeInactive: true }`.
282
+
283
+ ```typescript
284
+ const defs = await os.features.listForProduct()
285
+ for (const f of defs) {
286
+ console.log(`${f.code}: ${f.name}${f.hasQuantity ? " (metered)" : ""}`)
287
+ }
288
+ ```
289
+
278
290
  #### `features.setAccess(input)`
279
291
 
280
292
  Update feature access with **partial config merge**.
@@ -335,6 +347,34 @@ if (access.canUse) {
335
347
 
336
348
  Track usage, claim subscriptions, and manage billing.
337
349
 
350
+ #### `billing.getDashboardContext(input)`
351
+
352
+ **Recommended** for billing/usage dashboards when you only have your tenant’s `externalOrgId`. One round-trip returns `companyId`, `productLinkId`, and an ordered list of features with usage, limits, optional Stripe `billing`, and `billingDegraded` when usage exists without a linked subscription item.
353
+
354
+ Ordering uses `config_values.dashboard_order` or `dashboardOrder` when set; otherwise feature `displayOrder`.
355
+
356
+ ```typescript
357
+ const ctx = await os.billing.getDashboardContext({
358
+ externalOrgId: "org_abc123",
359
+ })
360
+
361
+ console.log(ctx.companyId, ctx.productLinkId)
362
+ for (const f of ctx.features) {
363
+ console.log(
364
+ f.featureCode,
365
+ f.currentUsage,
366
+ f.remaining,
367
+ f.canUse,
368
+ f.billingDegraded ? "(no Stripe period)" : "",
369
+ f.billing?.currentPeriodEnd
370
+ )
371
+ }
372
+ ```
373
+
374
+ If the tenant is not linked, the call fails with `NOT_FOUND` (same message style as other `externalOrgId` flows).
375
+
376
+ See [Changelog](#changelog) for migrating from `getByProductLink` + `listAccess` and hardcoded feature codes.
377
+
338
378
  #### `billing.trackUsage(input)`
339
379
 
340
380
  Track usage for a metered feature.
@@ -749,6 +789,10 @@ import type {
749
789
  ContactInput,
750
790
  FeatureCheckInput,
751
791
  FeatureSetAccessInput,
792
+ BillingDashboardContext,
793
+ GetBillingDashboardContextInput,
794
+ ListProductFeatureCatalogInput,
795
+ ProductFeatureCatalogItem,
752
796
  TrackUsageInput,
753
797
  OnboardInput,
754
798
  OnboardResult,
@@ -827,6 +871,32 @@ try {
827
871
 
828
872
  ---
829
873
 
874
+ ## Changelog
875
+
876
+ Release notes live in **[CHANGELOG.md](./CHANGELOG.md)**.
877
+
878
+ ### Dashboard context migration (PRD #194)
879
+
880
+ **Before:** Resolve the company, then list access and filter client-side:
881
+
882
+ ```typescript
883
+ const company = await os.companies.getByProductLink({ externalOrgId })
884
+ if (!company) {
885
+ /* onboarding */
886
+ }
887
+ const rows = await os.features.listAccess({
888
+ companyId: company.id,
889
+ includeBillingDetails: true,
890
+ })
891
+ // Often: pick one featureCode in code, merge catalog labels manually
892
+ ```
893
+
894
+ **After:** Single product-key call keyed by tenant id; use `features.listForProduct()` for definition labels and `os.billing.getDashboardContext({ externalOrgId })` for per-tenant state. Server applies link disambiguation, row shape, optional `billing`, `billingDegraded`, and dashboard ordering from OS `config_values`.
895
+
896
+ `features.listAccess` / `getAccessByExternalOrgId` remain supported; opt in when you need the new contract.
897
+
898
+ ---
899
+
830
900
  ## Related Documentation
831
901
 
832
902
  - **[QUICKSTART.md](./QUICKSTART.md)** — Quick publishing & installation guide
@@ -0,0 +1,16 @@
1
+ import type { BillingDashboardContext, GetBillingDashboardContextInput } from './types/billing-dashboard-context.js';
2
+ type DashboardCaller = <T>(operation: string, fn: () => Promise<T>) => Promise<T>;
3
+ type MinimalBillingClient = {
4
+ billing: {
5
+ getDashboardContext: {
6
+ query: (input: GetBillingDashboardContextInput) => Promise<unknown>;
7
+ };
8
+ };
9
+ };
10
+ /**
11
+ * Bound as `ChatarminOS.billing.getDashboardContext`.
12
+ * One call: resolve link by `externalOrgId`, return feature rows with usage, optional billing, `billingDegraded`.
13
+ */
14
+ export declare function bindBillingGetDashboardContext(call: DashboardCaller, client: MinimalBillingClient): (input: GetBillingDashboardContextInput) => Promise<BillingDashboardContext>;
15
+ export {};
16
+ //# sourceMappingURL=billing-get-dashboard-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"billing-get-dashboard-context.d.ts","sourceRoot":"","sources":["../src/billing-get-dashboard-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,uBAAuB,EACvB,+BAA+B,EAChC,MAAM,sCAAsC,CAAA;AAE7C,KAAK,eAAe,GAAG,CAAC,CAAC,EACvB,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,KACjB,OAAO,CAAC,CAAC,CAAC,CAAA;AAEf,KAAK,oBAAoB,GAAG;IAC1B,OAAO,EAAE;QACP,mBAAmB,EAAE;YACnB,KAAK,EAAE,CAAC,KAAK,EAAE,+BAA+B,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;SACpE,CAAA;KACF,CAAA;CACF,CAAA;AAED;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,eAAe,EACrB,MAAM,EAAE,oBAAoB,GAC3B,CACD,KAAK,EAAE,+BAA+B,KACnC,OAAO,CAAC,uBAAuB,CAAC,CAOpC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Bound as `ChatarminOS.billing.getDashboardContext`.
3
+ * One call: resolve link by `externalOrgId`, return feature rows with usage, optional billing, `billingDegraded`.
4
+ */
5
+ export function bindBillingGetDashboardContext(call, client) {
6
+ return (input) => call('billing.getDashboardContext', () => client.billing.getDashboardContext.query(input));
7
+ }
@@ -0,0 +1,122 @@
1
+ import type { AppRouter } from '@chatarmin/api';
2
+ import type { createTRPCClient } from '@trpc/client';
3
+ import type { ClaimCheckoutInput, EnsureFeatureSubscriptionInput, EnsureFeatureSubscriptionResult, LinkSubscriptionInput, RelinkBillingProfileInput, SetUsageInput, SyncCompanyFromStripeInput, SyncCompanyFromStripeResult, UsageResult } from './types/chatarmin-os-billing-inputs.js';
4
+ export type ChatarminOsBillingCall = <T>(operation: string, fn: () => Promise<T>) => Promise<T>;
5
+ export type ChatarminOsTrpcClient = ReturnType<typeof createTRPCClient<AppRouter>>;
6
+ /**
7
+ * Billing, usage tracking, and subscription management (`os.billing`).
8
+ */
9
+ export declare function createChatarminOsBillingNamespace(call: ChatarminOsBillingCall, client: ChatarminOsTrpcClient): {
10
+ getDashboardContext: (input: import("./index.js").GetBillingDashboardContextInput) => Promise<import("./index.js").BillingDashboardContext>;
11
+ setUsage: (input: SetUsageInput) => Promise<UsageResult>;
12
+ getUsageHistory: (companyId: string, featureCode: string, options?: {
13
+ limit?: number;
14
+ includeDelivered?: boolean;
15
+ eventType?: "increment" | "set" | "adjustment";
16
+ }) => Promise<{
17
+ id: string;
18
+ created_at: Date;
19
+ metadata: unknown;
20
+ event_type: string;
21
+ source: string;
22
+ billing_profile_id: string | null;
23
+ idempotency_key: string;
24
+ subscription_item_id: string | null;
25
+ feature_access_id: string | null;
26
+ meter_code: string;
27
+ quantity: number;
28
+ stripe_price_id: string | null;
29
+ event_time: Date;
30
+ previous_value: number | null;
31
+ adjustment_reason: string | null;
32
+ adjusts_event_id: string | null;
33
+ stripe_delivery_status: string;
34
+ stripe_delivery_attempts: number;
35
+ stripe_last_delivery_attempt_at: Date | null;
36
+ stripe_event_id: string | null;
37
+ stripe_delivery_error: string | null;
38
+ delivered_at: Date | null;
39
+ }[]>;
40
+ claimCheckout: (input: ClaimCheckoutInput) => Promise<{
41
+ success: boolean;
42
+ subscriptionId: string;
43
+ alreadyClaimed: boolean;
44
+ status?: undefined;
45
+ } | {
46
+ success: boolean;
47
+ subscriptionId: string;
48
+ alreadyClaimed: boolean;
49
+ status: import("stripe").Stripe.Subscription.Status;
50
+ }>;
51
+ linkSubscription: (input: LinkSubscriptionInput) => Promise<{
52
+ success: boolean;
53
+ subscriptionId: string;
54
+ alreadyLinked: boolean;
55
+ status?: undefined;
56
+ tierApplied?: undefined;
57
+ featuresApplied?: undefined;
58
+ } | {
59
+ success: boolean;
60
+ subscriptionId: string;
61
+ alreadyLinked: boolean;
62
+ status: import("stripe").Stripe.Subscription.Status;
63
+ tierApplied: boolean;
64
+ featuresApplied: number;
65
+ }>;
66
+ getStatus: (companyId: string) => Promise<{
67
+ hasBillingProfile: boolean;
68
+ stripeCustomerId: string | null;
69
+ billingProfiles: {
70
+ id: string;
71
+ organization_id: string;
72
+ created_at: Date;
73
+ updated_at: Date;
74
+ short_id: string | null;
75
+ name: string;
76
+ metadata: unknown;
77
+ status: string;
78
+ mrr_cents: number;
79
+ mrr_committed_cents: number;
80
+ mrr_usage_cents: number;
81
+ contract_start_at: Date | null;
82
+ contract_end_at: Date | null;
83
+ subscription_status: string;
84
+ max_days_overdue: number;
85
+ company_id: string | null;
86
+ stripe_customer_id: string;
87
+ link_status: string;
88
+ default_currency: string;
89
+ has_payment_method: boolean;
90
+ open_invoices_count: number;
91
+ open_amount_cents: number;
92
+ past_due_invoices_count: number;
93
+ past_due_amount_cents: number;
94
+ last_synced_at: Date | null;
95
+ company_group_id: string | null;
96
+ }[];
97
+ activeSubscriptions: {
98
+ id: string;
99
+ billingProfileId: string;
100
+ status: string;
101
+ stripeSubscriptionId: string | null;
102
+ currentPeriodEnd: Date | null;
103
+ trialEnd: Date | null;
104
+ tier: {
105
+ code: string;
106
+ name: string | null;
107
+ } | null;
108
+ }[];
109
+ enabledFeaturesCount: number;
110
+ }>;
111
+ syncCompanyFromStripe: (input: SyncCompanyFromStripeInput) => Promise<SyncCompanyFromStripeResult>;
112
+ relinkBillingProfile: (input: RelinkBillingProfileInput) => Promise<{
113
+ billingProfileId: string;
114
+ stripeCustomerId: string;
115
+ subscriptionsSynced: number;
116
+ subscriptionItemsSynced: number;
117
+ featuresLinked: number;
118
+ matchedMeters: number;
119
+ }>;
120
+ ensureFeatureSubscription: (input: EnsureFeatureSubscriptionInput) => Promise<EnsureFeatureSubscriptionResult>;
121
+ };
122
+ //# sourceMappingURL=chatarmin-os-billing-namespace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chatarmin-os-billing-namespace.d.ts","sourceRoot":"","sources":["../src/chatarmin-os-billing-namespace.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAEpD,OAAO,KAAK,EACV,kBAAkB,EAClB,8BAA8B,EAC9B,+BAA+B,EAC/B,qBAAqB,EACrB,yBAAyB,EACzB,aAAa,EACb,0BAA0B,EAC1B,2BAA2B,EAC3B,WAAW,EACZ,MAAM,wCAAwC,CAAA;AAE/C,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,EACrC,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,KACjB,OAAO,CAAC,CAAC,CAAC,CAAA;AAEf,MAAM,MAAM,qBAAqB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,SAAS,CAAC,CAAC,CAAA;AAElF;;GAEG;AACH,wBAAgB,iCAAiC,CAC/C,IAAI,EAAE,sBAAsB,EAC5B,MAAM,EAAE,qBAAqB;;sBAKT,aAAa,KAAG,OAAO,CAAC,WAAW,CAAC;iCAWzC,MAAM,eACJ,MAAM,YACT;QACR,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,gBAAgB,CAAC,EAAE,OAAO,CAAA;QAC1B,SAAS,CAAC,EAAE,WAAW,GAAG,KAAK,GAAG,YAAY,CAAA;KAC/C;;;;;;;;;;;;;;;;;;;;;;;;2BAUoB,kBAAkB;;;;;;;;;;;8BAKf,qBAAqB;;;;;;;;;;;;;;;2BAKxB,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mCAMpB,0BAA0B,KAChC,OAAO,CAAC,2BAA2B,CAAC;kCAKT,yBAAyB;;;;;;;;uCAM9C,8BAA8B,KACpC,OAAO,CAAC,+BAA+B,CAAC;EAK9C"}
@@ -0,0 +1,24 @@
1
+ import { bindBillingGetDashboardContext } from './billing-get-dashboard-context.js';
2
+ /**
3
+ * Billing, usage tracking, and subscription management (`os.billing`).
4
+ */
5
+ export function createChatarminOsBillingNamespace(call, client) {
6
+ return {
7
+ getDashboardContext: bindBillingGetDashboardContext(call, client),
8
+ setUsage: (input) => call("billing.setUsage", () => client.billing.setUsage.mutate({
9
+ ...input,
10
+ source: "api",
11
+ })),
12
+ getUsageHistory: (companyId, featureCode, options) => call("billing.getUsageHistory", () => client.billing.getUsageHistory.query({
13
+ companyId,
14
+ featureCode,
15
+ ...options,
16
+ })),
17
+ claimCheckout: (input) => call("billing.claimCheckout", () => client.billing.claimCheckout.mutate(input)),
18
+ linkSubscription: (input) => call("billing.linkSubscription", () => client.billing.linkSubscription.mutate(input)),
19
+ getStatus: (companyId) => call("billing.getStatus", () => client.billing.getStatus.query({ companyId })),
20
+ syncCompanyFromStripe: (input) => call("billing.syncCompanyFromStripe", () => client.billing.syncCompanyFromStripe.mutate(input)),
21
+ relinkBillingProfile: (input) => call("billing.relinkBillingProfile", () => client.billing.relinkBillingProfile.mutate(input)),
22
+ ensureFeatureSubscription: (input) => call("billing.ensureFeatureSubscription", () => client.billing.ensureFeatureSubscription.mutate(input)),
23
+ };
24
+ }
@@ -0,0 +1,26 @@
1
+ import type { ListProductFeatureCatalogInput, ProductFeatureCatalogItem } from './types/product-feature-catalog.js';
2
+ type ListForProductCaller = <T>(operation: string, fn: () => Promise<T>) => Promise<T>;
3
+ type MinimalFeaturesClient = {
4
+ features: {
5
+ listForProduct: {
6
+ query: (input: {
7
+ includeInactive: boolean;
8
+ }) => Promise<unknown>;
9
+ };
10
+ };
11
+ };
12
+ /**
13
+ * Bound as `ChatarminOSClient.features.listForProduct`.
14
+ * Lists feature definitions for the API key’s product (no company id).
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * const defs = await os.features.listForProduct()
19
+ * for (const f of defs) {
20
+ * console.log(`${f.code}: ${f.name}${f.hasQuantity ? ' (metered)' : ''}`)
21
+ * }
22
+ * ```
23
+ */
24
+ export declare function bindFeaturesListForProduct(call: ListForProductCaller, client: MinimalFeaturesClient): (input?: ListProductFeatureCatalogInput) => Promise<ProductFeatureCatalogItem[]>;
25
+ export {};
26
+ //# sourceMappingURL=features-list-for-product.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"features-list-for-product.d.ts","sourceRoot":"","sources":["../src/features-list-for-product.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,8BAA8B,EAC9B,yBAAyB,EAC1B,MAAM,oCAAoC,CAAA;AAE3C,KAAK,oBAAoB,GAAG,CAAC,CAAC,EAC5B,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,KACjB,OAAO,CAAC,CAAC,CAAC,CAAA;AAEf,KAAK,qBAAqB,GAAG;IAC3B,QAAQ,EAAE;QACR,cAAc,EAAE;YACd,KAAK,EAAE,CAAC,KAAK,EAAE;gBAAE,eAAe,EAAE,OAAO,CAAA;aAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;SACjE,CAAA;KACF,CAAA;CACF,CAAA;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,oBAAoB,EAC1B,MAAM,EAAE,qBAAqB,GAC5B,CAAC,KAAK,CAAC,EAAE,8BAA8B,KAAK,OAAO,CAAC,yBAAyB,EAAE,CAAC,CAOlF"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Bound as `ChatarminOSClient.features.listForProduct`.
3
+ * Lists feature definitions for the API key’s product (no company id).
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * const defs = await os.features.listForProduct()
8
+ * for (const f of defs) {
9
+ * console.log(`${f.code}: ${f.name}${f.hasQuantity ? ' (metered)' : ''}`)
10
+ * }
11
+ * ```
12
+ */
13
+ export function bindFeaturesListForProduct(call, client) {
14
+ return (input) => call('features.listForProduct', () => client.features.listForProduct.query({
15
+ includeInactive: input?.includeInactive ?? false,
16
+ }));
17
+ }