@usebillow/sdk 0.5.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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +274 -0
- package/dist/billing-C4RIMgH_.d.ts +1053 -0
- package/dist/billing-DZ4rIyg7.d.cts +1053 -0
- package/dist/billing-status-BZQN_gm7.d.cts +29 -0
- package/dist/billing-status-BZQN_gm7.d.ts +29 -0
- package/dist/chunk-CCG4F5FK.js +48 -0
- package/dist/chunk-CCG4F5FK.js.map +1 -0
- package/dist/chunk-Z6VXPONT.js +1493 -0
- package/dist/chunk-Z6VXPONT.js.map +1 -0
- package/dist/config.cjs +233 -0
- package/dist/config.cjs.map +1 -0
- package/dist/config.d.cts +104 -0
- package/dist/config.d.ts +104 -0
- package/dist/config.js +228 -0
- package/dist/config.js.map +1 -0
- package/dist/credits-C3Fe3TO0.d.cts +315 -0
- package/dist/credits-C3Fe3TO0.d.ts +315 -0
- package/dist/index.cjs +1560 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2379 -0
- package/dist/index.d.ts +2379 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/ingestion.cjs +259 -0
- package/dist/ingestion.cjs.map +1 -0
- package/dist/ingestion.d.cts +182 -0
- package/dist/ingestion.d.ts +182 -0
- package/dist/ingestion.js +252 -0
- package/dist/ingestion.js.map +1 -0
- package/dist/react.cjs +360 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +71 -0
- package/dist/react.d.ts +71 -0
- package/dist/react.js +153 -0
- package/dist/react.js.map +1 -0
- package/dist/server.cjs +98 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +54 -0
- package/dist/server.d.ts +54 -0
- package/dist/server.js +96 -0
- package/dist/server.js.map +1 -0
- package/dist/status.cjs +60 -0
- package/dist/status.cjs.map +1 -0
- package/dist/status.d.cts +31 -0
- package/dist/status.d.ts +31 -0
- package/dist/status.js +3 -0
- package/dist/status.js.map +1 -0
- package/dist/webhooks.cjs +157 -0
- package/dist/webhooks.cjs.map +1 -0
- package/dist/webhooks.d.cts +391 -0
- package/dist/webhooks.d.ts +391 -0
- package/dist/webhooks.js +143 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +169 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Billow } from './index.cjs';
|
|
2
|
+
import { F as FeatureKind, M as MeterConfig, C as CreateProductInput, a as CreateCouponInput } from './billing-DZ4rIyg7.cjs';
|
|
3
|
+
import './billing-status-BZQN_gm7.cjs';
|
|
4
|
+
import './status.cjs';
|
|
5
|
+
import './credits-C3Fe3TO0.cjs';
|
|
6
|
+
import 'zod';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Code-as-config sync (ARCHITECTURE §12 "code-as-config sync"). Keep your catalog
|
|
10
|
+
* — features, products, coupons — in a typed config file and `syncConfig` it into
|
|
11
|
+
* a billow server. Sync is **declarative and additive**: it creates whatever is
|
|
12
|
+
* missing and *reports drift* on what already exists, but never deletes and never
|
|
13
|
+
* rewrites an existing product (catalog edits are made deliberately through the
|
|
14
|
+
* dashboard/API — ADR-0015 — not silently on every sync).
|
|
15
|
+
*
|
|
16
|
+
* import { defineConfig, syncConfig } from "@usebillow/sdk/config";
|
|
17
|
+
* const config = defineConfig({ features: [...], products: [...], coupons: [...] });
|
|
18
|
+
* await syncConfig(billow, config, { onProgress: console.log });
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
interface ConfigFeature {
|
|
22
|
+
slug: string;
|
|
23
|
+
name: string;
|
|
24
|
+
kind: FeatureKind;
|
|
25
|
+
/**
|
|
26
|
+
* For a `metered` feature: how its usage aggregates (ADR-0011). Omit to get a `sum`
|
|
27
|
+
* meter. Set it here so sync creates the meter you want instead of the default — after
|
|
28
|
+
* usage is recorded the aggregation can no longer change.
|
|
29
|
+
*/
|
|
30
|
+
meter?: MeterConfig;
|
|
31
|
+
}
|
|
32
|
+
/** A product entry — the same shape as `billow.products.create`. */
|
|
33
|
+
type ConfigProduct = CreateProductInput;
|
|
34
|
+
/**
|
|
35
|
+
* A coupon entry. Unlike `billow.coupons.create`, the `code` is **required** — it
|
|
36
|
+
* is the stable identity sync diffs against (a code-less coupon can't be matched).
|
|
37
|
+
*/
|
|
38
|
+
type ConfigCoupon = Omit<CreateCouponInput, "code"> & {
|
|
39
|
+
code: string;
|
|
40
|
+
};
|
|
41
|
+
interface BillowConfig {
|
|
42
|
+
features?: ConfigFeature[];
|
|
43
|
+
products?: ConfigProduct[];
|
|
44
|
+
coupons?: ConfigCoupon[];
|
|
45
|
+
}
|
|
46
|
+
/** Identity helper — returns its argument, typed, for editor completion + checks. */
|
|
47
|
+
declare function defineConfig(config: BillowConfig): BillowConfig;
|
|
48
|
+
type SyncAction = "create" | "unchanged" | "drift";
|
|
49
|
+
interface SyncItem {
|
|
50
|
+
kind: "feature" | "product" | "coupon";
|
|
51
|
+
/** Slug (features/products) or code (coupons). */
|
|
52
|
+
id: string;
|
|
53
|
+
action: SyncAction;
|
|
54
|
+
/** For `drift`: which aspects differ from the server (human-readable). */
|
|
55
|
+
changes?: string[];
|
|
56
|
+
}
|
|
57
|
+
interface SyncPlan {
|
|
58
|
+
features: SyncItem[];
|
|
59
|
+
products: SyncItem[];
|
|
60
|
+
coupons: SyncItem[];
|
|
61
|
+
}
|
|
62
|
+
interface SyncResult {
|
|
63
|
+
plan: SyncPlan;
|
|
64
|
+
created: {
|
|
65
|
+
features: string[];
|
|
66
|
+
products: string[];
|
|
67
|
+
coupons: string[];
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Items that still differ from the config after the sync - reported, not applied. With
|
|
71
|
+
* `applyMetadata`, product metadata that was written is removed from here (see `applied`).
|
|
72
|
+
*/
|
|
73
|
+
drift: SyncItem[];
|
|
74
|
+
/** Product slugs whose metadata was written this run (only when `applyMetadata` is set). */
|
|
75
|
+
applied: {
|
|
76
|
+
productMetadata: string[];
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Diff a config against the server without writing anything. Each entry is
|
|
81
|
+
* `create` (missing), `unchanged` (matches), or `drift` (exists but differs).
|
|
82
|
+
*/
|
|
83
|
+
declare function planConfig(billow: Billow, config: BillowConfig): Promise<SyncPlan>;
|
|
84
|
+
/**
|
|
85
|
+
* Apply a config: create everything missing (features → products → coupons, the
|
|
86
|
+
* dependency order, since products reference feature slugs), and return what was
|
|
87
|
+
* created plus any drift it found.
|
|
88
|
+
*
|
|
89
|
+
* Structural drift (prices, entitlements, name, ...) is never auto-applied - those edits are
|
|
90
|
+
* version-bearing (ADR-0015: collected at renewal) and belong to a deliberate `products.update`.
|
|
91
|
+
* With `applyMetadata: true`, product **metadata** drift IS written, because metadata lives on
|
|
92
|
+
* the product row (not the version), so updating it is version-independent and cannot rewrite
|
|
93
|
+
* billed history. This closes the write-side gap where a config's product metadata could only
|
|
94
|
+
* be reported, never pushed. The write is a full-map replace (config is authoritative): a key
|
|
95
|
+
* present on the server but absent from the config is cleared.
|
|
96
|
+
*/
|
|
97
|
+
declare function syncConfig(billow: Billow, config: BillowConfig, opts?: {
|
|
98
|
+
onProgress?: (message: string) => void;
|
|
99
|
+
applyMetadata?: boolean;
|
|
100
|
+
}): Promise<SyncResult>;
|
|
101
|
+
/** A one-line-per-item summary of a plan, for CLI output. */
|
|
102
|
+
declare function formatPlan(plan: SyncPlan): string;
|
|
103
|
+
|
|
104
|
+
export { type BillowConfig, type ConfigCoupon, type ConfigFeature, type ConfigProduct, type SyncAction, type SyncItem, type SyncPlan, type SyncResult, defineConfig, formatPlan, planConfig, syncConfig };
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Billow } from './index.js';
|
|
2
|
+
import { F as FeatureKind, M as MeterConfig, C as CreateProductInput, a as CreateCouponInput } from './billing-C4RIMgH_.js';
|
|
3
|
+
import './billing-status-BZQN_gm7.js';
|
|
4
|
+
import './status.js';
|
|
5
|
+
import './credits-C3Fe3TO0.js';
|
|
6
|
+
import 'zod';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Code-as-config sync (ARCHITECTURE §12 "code-as-config sync"). Keep your catalog
|
|
10
|
+
* — features, products, coupons — in a typed config file and `syncConfig` it into
|
|
11
|
+
* a billow server. Sync is **declarative and additive**: it creates whatever is
|
|
12
|
+
* missing and *reports drift* on what already exists, but never deletes and never
|
|
13
|
+
* rewrites an existing product (catalog edits are made deliberately through the
|
|
14
|
+
* dashboard/API — ADR-0015 — not silently on every sync).
|
|
15
|
+
*
|
|
16
|
+
* import { defineConfig, syncConfig } from "@usebillow/sdk/config";
|
|
17
|
+
* const config = defineConfig({ features: [...], products: [...], coupons: [...] });
|
|
18
|
+
* await syncConfig(billow, config, { onProgress: console.log });
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
interface ConfigFeature {
|
|
22
|
+
slug: string;
|
|
23
|
+
name: string;
|
|
24
|
+
kind: FeatureKind;
|
|
25
|
+
/**
|
|
26
|
+
* For a `metered` feature: how its usage aggregates (ADR-0011). Omit to get a `sum`
|
|
27
|
+
* meter. Set it here so sync creates the meter you want instead of the default — after
|
|
28
|
+
* usage is recorded the aggregation can no longer change.
|
|
29
|
+
*/
|
|
30
|
+
meter?: MeterConfig;
|
|
31
|
+
}
|
|
32
|
+
/** A product entry — the same shape as `billow.products.create`. */
|
|
33
|
+
type ConfigProduct = CreateProductInput;
|
|
34
|
+
/**
|
|
35
|
+
* A coupon entry. Unlike `billow.coupons.create`, the `code` is **required** — it
|
|
36
|
+
* is the stable identity sync diffs against (a code-less coupon can't be matched).
|
|
37
|
+
*/
|
|
38
|
+
type ConfigCoupon = Omit<CreateCouponInput, "code"> & {
|
|
39
|
+
code: string;
|
|
40
|
+
};
|
|
41
|
+
interface BillowConfig {
|
|
42
|
+
features?: ConfigFeature[];
|
|
43
|
+
products?: ConfigProduct[];
|
|
44
|
+
coupons?: ConfigCoupon[];
|
|
45
|
+
}
|
|
46
|
+
/** Identity helper — returns its argument, typed, for editor completion + checks. */
|
|
47
|
+
declare function defineConfig(config: BillowConfig): BillowConfig;
|
|
48
|
+
type SyncAction = "create" | "unchanged" | "drift";
|
|
49
|
+
interface SyncItem {
|
|
50
|
+
kind: "feature" | "product" | "coupon";
|
|
51
|
+
/** Slug (features/products) or code (coupons). */
|
|
52
|
+
id: string;
|
|
53
|
+
action: SyncAction;
|
|
54
|
+
/** For `drift`: which aspects differ from the server (human-readable). */
|
|
55
|
+
changes?: string[];
|
|
56
|
+
}
|
|
57
|
+
interface SyncPlan {
|
|
58
|
+
features: SyncItem[];
|
|
59
|
+
products: SyncItem[];
|
|
60
|
+
coupons: SyncItem[];
|
|
61
|
+
}
|
|
62
|
+
interface SyncResult {
|
|
63
|
+
plan: SyncPlan;
|
|
64
|
+
created: {
|
|
65
|
+
features: string[];
|
|
66
|
+
products: string[];
|
|
67
|
+
coupons: string[];
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Items that still differ from the config after the sync - reported, not applied. With
|
|
71
|
+
* `applyMetadata`, product metadata that was written is removed from here (see `applied`).
|
|
72
|
+
*/
|
|
73
|
+
drift: SyncItem[];
|
|
74
|
+
/** Product slugs whose metadata was written this run (only when `applyMetadata` is set). */
|
|
75
|
+
applied: {
|
|
76
|
+
productMetadata: string[];
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Diff a config against the server without writing anything. Each entry is
|
|
81
|
+
* `create` (missing), `unchanged` (matches), or `drift` (exists but differs).
|
|
82
|
+
*/
|
|
83
|
+
declare function planConfig(billow: Billow, config: BillowConfig): Promise<SyncPlan>;
|
|
84
|
+
/**
|
|
85
|
+
* Apply a config: create everything missing (features → products → coupons, the
|
|
86
|
+
* dependency order, since products reference feature slugs), and return what was
|
|
87
|
+
* created plus any drift it found.
|
|
88
|
+
*
|
|
89
|
+
* Structural drift (prices, entitlements, name, ...) is never auto-applied - those edits are
|
|
90
|
+
* version-bearing (ADR-0015: collected at renewal) and belong to a deliberate `products.update`.
|
|
91
|
+
* With `applyMetadata: true`, product **metadata** drift IS written, because metadata lives on
|
|
92
|
+
* the product row (not the version), so updating it is version-independent and cannot rewrite
|
|
93
|
+
* billed history. This closes the write-side gap where a config's product metadata could only
|
|
94
|
+
* be reported, never pushed. The write is a full-map replace (config is authoritative): a key
|
|
95
|
+
* present on the server but absent from the config is cleared.
|
|
96
|
+
*/
|
|
97
|
+
declare function syncConfig(billow: Billow, config: BillowConfig, opts?: {
|
|
98
|
+
onProgress?: (message: string) => void;
|
|
99
|
+
applyMetadata?: boolean;
|
|
100
|
+
}): Promise<SyncResult>;
|
|
101
|
+
/** A one-line-per-item summary of a plan, for CLI output. */
|
|
102
|
+
declare function formatPlan(plan: SyncPlan): string;
|
|
103
|
+
|
|
104
|
+
export { type BillowConfig, type ConfigCoupon, type ConfigFeature, type ConfigProduct, type SyncAction, type SyncItem, type SyncPlan, type SyncResult, defineConfig, formatPlan, planConfig, syncConfig };
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
function defineConfig(config) {
|
|
3
|
+
return config;
|
|
4
|
+
}
|
|
5
|
+
async function planConfig(billow, config) {
|
|
6
|
+
const [features, products, coupons] = await Promise.all([
|
|
7
|
+
billow.features.list({ limit: 100 }).listAll(),
|
|
8
|
+
billow.products.list(),
|
|
9
|
+
billow.coupons.list({ limit: 100 }).listAll()
|
|
10
|
+
]);
|
|
11
|
+
const featureBySlug = new Map(features.map((f) => [f.slug, f]));
|
|
12
|
+
const productBySlug = new Map(products.map((p) => [p.slug, p]));
|
|
13
|
+
const couponByCode = new Map(coupons.filter((c) => c.code).map((c) => [c.code, c]));
|
|
14
|
+
return {
|
|
15
|
+
features: (config.features ?? []).map((f) => planFeature(f, featureBySlug.get(f.slug))),
|
|
16
|
+
products: (config.products ?? []).map((p) => planProduct(p, productBySlug.get(p.slug))),
|
|
17
|
+
coupons: (config.coupons ?? []).map((c) => planCoupon(c, couponByCode.get(c.code)))
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
async function syncConfig(billow, config, opts = {}) {
|
|
21
|
+
const plan = await planConfig(billow, config);
|
|
22
|
+
const progress = opts.onProgress ?? (() => {
|
|
23
|
+
});
|
|
24
|
+
const created = {
|
|
25
|
+
features: await createMissing(
|
|
26
|
+
plan.features,
|
|
27
|
+
config.features ?? [],
|
|
28
|
+
(f) => f.slug,
|
|
29
|
+
"feature",
|
|
30
|
+
(f) => billow.features.create(f),
|
|
31
|
+
progress
|
|
32
|
+
),
|
|
33
|
+
products: await createMissing(
|
|
34
|
+
plan.products,
|
|
35
|
+
config.products ?? [],
|
|
36
|
+
(p) => p.slug,
|
|
37
|
+
"product",
|
|
38
|
+
(p) => billow.products.create(p),
|
|
39
|
+
progress
|
|
40
|
+
),
|
|
41
|
+
coupons: await createMissing(
|
|
42
|
+
plan.coupons,
|
|
43
|
+
config.coupons ?? [],
|
|
44
|
+
(c) => c.code,
|
|
45
|
+
"coupon",
|
|
46
|
+
(c) => billow.coupons.create(c),
|
|
47
|
+
progress
|
|
48
|
+
)
|
|
49
|
+
};
|
|
50
|
+
const applied = { productMetadata: [] };
|
|
51
|
+
if (opts.applyMetadata) {
|
|
52
|
+
const isMetadataChange = (c) => c.startsWith("metadata");
|
|
53
|
+
const productDefBySlug = new Map((config.products ?? []).map((p) => [p.slug, p]));
|
|
54
|
+
for (const item of plan.products) {
|
|
55
|
+
if (item.action !== "drift" || !item.changes?.some(isMetadataChange)) continue;
|
|
56
|
+
const def = productDefBySlug.get(item.id);
|
|
57
|
+
if (!def) continue;
|
|
58
|
+
await billow.products.update(item.id, { metadata: def.metadata ?? {} });
|
|
59
|
+
applied.productMetadata.push(item.id);
|
|
60
|
+
progress(`~ product ${item.id} (metadata applied)`);
|
|
61
|
+
item.changes = item.changes.filter((c) => !isMetadataChange(c));
|
|
62
|
+
if (item.changes.length === 0) item.action = "unchanged";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const drift = [...plan.features, ...plan.products, ...plan.coupons].filter(
|
|
66
|
+
(i) => i.action === "drift"
|
|
67
|
+
);
|
|
68
|
+
return { plan, created, drift, applied };
|
|
69
|
+
}
|
|
70
|
+
async function createMissing(items, defs, idOf, label, create, progress) {
|
|
71
|
+
const byId = new Map(defs.map((d) => [idOf(d), d]));
|
|
72
|
+
const created = [];
|
|
73
|
+
for (const item of items) {
|
|
74
|
+
if (item.action !== "create") continue;
|
|
75
|
+
const def = byId.get(item.id);
|
|
76
|
+
if (!def) continue;
|
|
77
|
+
await create(def);
|
|
78
|
+
created.push(item.id);
|
|
79
|
+
progress(`+ ${label} ${item.id}`);
|
|
80
|
+
}
|
|
81
|
+
return created;
|
|
82
|
+
}
|
|
83
|
+
function formatPlan(plan) {
|
|
84
|
+
const mark = { create: "+", unchanged: "=", drift: "~" };
|
|
85
|
+
const lines = [];
|
|
86
|
+
for (const item of [...plan.features, ...plan.products, ...plan.coupons]) {
|
|
87
|
+
const detail = item.changes?.length ? ` (${item.changes.join(", ")})` : "";
|
|
88
|
+
lines.push(`${mark[item.action]} ${item.kind} ${item.id}${detail}`);
|
|
89
|
+
}
|
|
90
|
+
return lines.join("\n");
|
|
91
|
+
}
|
|
92
|
+
function planFeature(def, existing) {
|
|
93
|
+
if (!existing) return { kind: "feature", id: def.slug, action: "create" };
|
|
94
|
+
const changes = diffFields([
|
|
95
|
+
["name", def.name, existing.name],
|
|
96
|
+
["kind", def.kind, existing.kind],
|
|
97
|
+
// A metered feature with no explicit meter gets a `sum` meter server-side; normalize
|
|
98
|
+
// both sides so an aggregation change is reported as drift, not "unchanged".
|
|
99
|
+
[
|
|
100
|
+
"meter",
|
|
101
|
+
normMeter(def.kind, def.meter),
|
|
102
|
+
normMeter(existing.kind, existing.meter ?? void 0)
|
|
103
|
+
]
|
|
104
|
+
]);
|
|
105
|
+
return { kind: "feature", id: def.slug, action: changes.length ? "drift" : "unchanged", changes };
|
|
106
|
+
}
|
|
107
|
+
function planProduct(def, existing) {
|
|
108
|
+
if (!existing) return { kind: "product", id: def.slug, action: "create" };
|
|
109
|
+
const changes = diffFields([
|
|
110
|
+
["name", def.name, existing.name],
|
|
111
|
+
["type", def.type, existing.type],
|
|
112
|
+
["isAddOn", def.isAddOn ?? false, existing.isAddOn],
|
|
113
|
+
["metadata", normMeta(def.metadata), normMeta(existing.metadata)]
|
|
114
|
+
]);
|
|
115
|
+
if (!sameSet(def.prices.map(normPrice), (existing.prices ?? []).map(normPrice))) {
|
|
116
|
+
changes.push("prices");
|
|
117
|
+
}
|
|
118
|
+
if (!sameSet((def.entitlements ?? []).map(normEnt), (existing.entitlements ?? []).map(normEnt))) {
|
|
119
|
+
changes.push("entitlements");
|
|
120
|
+
}
|
|
121
|
+
return { kind: "product", id: def.slug, action: changes.length ? "drift" : "unchanged", changes };
|
|
122
|
+
}
|
|
123
|
+
function planCoupon(def, existing) {
|
|
124
|
+
if (!existing) return { kind: "coupon", id: def.code, action: "create" };
|
|
125
|
+
const changes = diffFields([
|
|
126
|
+
["kind", def.kind, existing.kind],
|
|
127
|
+
["value", def.value, existing.value],
|
|
128
|
+
["currency", def.currency?.toUpperCase() ?? null, existing.currency],
|
|
129
|
+
["duration", def.duration, existing.duration],
|
|
130
|
+
["durationCycles", def.durationCycles ?? null, existing.durationCycles],
|
|
131
|
+
["maxRedemptions", def.maxRedemptions ?? null, existing.maxRedemptions],
|
|
132
|
+
["expiresAt", epochOrNull(def.expiresAt), epochOrNull(existing.expiresAt)]
|
|
133
|
+
]);
|
|
134
|
+
if (!sameSet(def.productRestrictions ?? [], existing.productRestrictions ?? [])) {
|
|
135
|
+
changes.push("productRestrictions");
|
|
136
|
+
}
|
|
137
|
+
return { kind: "coupon", id: def.code, action: changes.length ? "drift" : "unchanged", changes };
|
|
138
|
+
}
|
|
139
|
+
function normMeta(metadata) {
|
|
140
|
+
const entries = Object.entries(metadata ?? {}).sort(
|
|
141
|
+
(a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0
|
|
142
|
+
);
|
|
143
|
+
return JSON.stringify(entries);
|
|
144
|
+
}
|
|
145
|
+
function normMeter(kind, meter) {
|
|
146
|
+
if (kind !== "metered") return null;
|
|
147
|
+
return JSON.stringify({
|
|
148
|
+
aggregation: meter?.aggregation ?? "sum",
|
|
149
|
+
eventProperty: meter?.eventProperty ?? null,
|
|
150
|
+
// The filter list and each filter's values are unordered sets — sort both so a mere
|
|
151
|
+
// reordering isn't reported as drift (matches the server's no-op semantics).
|
|
152
|
+
filters: (meter?.filters ?? []).map((f) => ({ key: f.key, values: [...f.values].sort() })).sort((a, b) => {
|
|
153
|
+
if (a.key !== b.key) return a.key < b.key ? -1 : 1;
|
|
154
|
+
const av = JSON.stringify(a.values);
|
|
155
|
+
const bv = JSON.stringify(b.values);
|
|
156
|
+
return av < bv ? -1 : av > bv ? 1 : 0;
|
|
157
|
+
}),
|
|
158
|
+
roundingMode: meter?.roundingMode ?? "none",
|
|
159
|
+
roundingScale: meter?.roundingScale ?? null,
|
|
160
|
+
// Reset anchoring (plan 07). Server omits these for cycle meters, so default to cycle/null
|
|
161
|
+
// on both sides — an omitted anchor and an explicit `cycle` compare equal (no false drift).
|
|
162
|
+
resetAnchor: meter?.resetAnchor ?? "cycle",
|
|
163
|
+
resetTz: meter?.resetTz ?? null
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function normPrice(p) {
|
|
167
|
+
return JSON.stringify({
|
|
168
|
+
kind: p.kind,
|
|
169
|
+
interval: p.interval ?? null,
|
|
170
|
+
intervalCount: p.intervalCount ?? 1,
|
|
171
|
+
amount: p.amount,
|
|
172
|
+
currency: p.currency.toUpperCase(),
|
|
173
|
+
meterFeature: p.meterFeature ?? null,
|
|
174
|
+
// Usage prices default to graduated server-side; normalize so a model change
|
|
175
|
+
// (or package/percentage param change) is detected as drift, not "unchanged".
|
|
176
|
+
usageModel: p.kind === "usage" ? p.usageModel ?? "graduated" : null,
|
|
177
|
+
usageTiers: canonTiers(p.usageTiers),
|
|
178
|
+
packageSize: p.packageSize ?? null,
|
|
179
|
+
rateBps: p.rateBps ?? null,
|
|
180
|
+
// Tax overrides (Phase E) — null = inherit org default; included so a change
|
|
181
|
+
// is detected as drift.
|
|
182
|
+
taxRateBps: p.taxRateBps ?? null,
|
|
183
|
+
taxBehavior: p.taxBehavior ?? null
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function canonTiers(tiers) {
|
|
187
|
+
if (!Array.isArray(tiers)) return null;
|
|
188
|
+
return tiers.map((t) => {
|
|
189
|
+
const tier = t ?? {};
|
|
190
|
+
return {
|
|
191
|
+
upTo: tier.upTo ?? null,
|
|
192
|
+
unitAmount: tier.unitAmount ?? null,
|
|
193
|
+
flatAmount: tier.flatAmount ?? null
|
|
194
|
+
};
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
function normEnt(e) {
|
|
198
|
+
return JSON.stringify({
|
|
199
|
+
feature: e.feature,
|
|
200
|
+
allowance: e.allowance ?? null,
|
|
201
|
+
resetInterval: e.resetInterval ?? null,
|
|
202
|
+
rollover: e.rollover ?? false
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
function diffFields(fields) {
|
|
206
|
+
const changes = [];
|
|
207
|
+
for (const [name, want, have] of fields) {
|
|
208
|
+
if (want !== have) changes.push(`${name}: ${fmt(have)} \u2192 ${fmt(want)}`);
|
|
209
|
+
}
|
|
210
|
+
return changes;
|
|
211
|
+
}
|
|
212
|
+
function sameSet(a, b) {
|
|
213
|
+
if (a.length !== b.length) return false;
|
|
214
|
+
const sortedA = [...a].sort();
|
|
215
|
+
const sortedB = [...b].sort();
|
|
216
|
+
return sortedA.every((v, i) => v === sortedB[i]);
|
|
217
|
+
}
|
|
218
|
+
function epochOrNull(value) {
|
|
219
|
+
return value ? new Date(value).getTime() : null;
|
|
220
|
+
}
|
|
221
|
+
function fmt(value) {
|
|
222
|
+
if (value === null || value === void 0) return "\u2205";
|
|
223
|
+
return String(value);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export { defineConfig, formatPlan, planConfig, syncConfig };
|
|
227
|
+
//# sourceMappingURL=config.js.map
|
|
228
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config.ts"],"names":[],"mappings":"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","file":"config.js","sourcesContent":["/**\n * Code-as-config sync (ARCHITECTURE §12 \"code-as-config sync\"). Keep your catalog\n * — features, products, coupons — in a typed config file and `syncConfig` it into\n * a billow server. Sync is **declarative and additive**: it creates whatever is\n * missing and *reports drift* on what already exists, but never deletes and never\n * rewrites an existing product (catalog edits are made deliberately through the\n * dashboard/API — ADR-0015 — not silently on every sync).\n *\n * import { defineConfig, syncConfig } from \"@usebillow/sdk/config\";\n * const config = defineConfig({ features: [...], products: [...], coupons: [...] });\n * await syncConfig(billow, config, { onProgress: console.log });\n */\nimport type {\n Billow,\n Coupon,\n CreateCouponInput,\n CreateProductInput,\n Feature,\n FeatureKind,\n MeterConfig,\n Product,\n} from \"./index\";\n\nexport interface ConfigFeature {\n slug: string;\n name: string;\n kind: FeatureKind;\n /**\n * For a `metered` feature: how its usage aggregates (ADR-0011). Omit to get a `sum`\n * meter. Set it here so sync creates the meter you want instead of the default — after\n * usage is recorded the aggregation can no longer change.\n */\n meter?: MeterConfig;\n}\n\n/** A product entry — the same shape as `billow.products.create`. */\nexport type ConfigProduct = CreateProductInput;\n\n/**\n * A coupon entry. Unlike `billow.coupons.create`, the `code` is **required** — it\n * is the stable identity sync diffs against (a code-less coupon can't be matched).\n */\nexport type ConfigCoupon = Omit<CreateCouponInput, \"code\"> & { code: string };\n\nexport interface BillowConfig {\n features?: ConfigFeature[];\n products?: ConfigProduct[];\n coupons?: ConfigCoupon[];\n}\n\n/** Identity helper — returns its argument, typed, for editor completion + checks. */\nexport function defineConfig(config: BillowConfig): BillowConfig {\n return config;\n}\n\nexport type SyncAction = \"create\" | \"unchanged\" | \"drift\";\n\nexport interface SyncItem {\n kind: \"feature\" | \"product\" | \"coupon\";\n /** Slug (features/products) or code (coupons). */\n id: string;\n action: SyncAction;\n /** For `drift`: which aspects differ from the server (human-readable). */\n changes?: string[];\n}\n\nexport interface SyncPlan {\n features: SyncItem[];\n products: SyncItem[];\n coupons: SyncItem[];\n}\n\nexport interface SyncResult {\n plan: SyncPlan;\n created: { features: string[]; products: string[]; coupons: string[] };\n /**\n * Items that still differ from the config after the sync - reported, not applied. With\n * `applyMetadata`, product metadata that was written is removed from here (see `applied`).\n */\n drift: SyncItem[];\n /** Product slugs whose metadata was written this run (only when `applyMetadata` is set). */\n applied: { productMetadata: string[] };\n}\n\n/**\n * Diff a config against the server without writing anything. Each entry is\n * `create` (missing), `unchanged` (matches), or `drift` (exists but differs).\n */\nexport async function planConfig(billow: Billow, config: BillowConfig): Promise<SyncPlan> {\n // Collect the whole catalog in as few round-trips as the server allows (page size caps at 100).\n const [features, products, coupons] = await Promise.all([\n billow.features.list({ limit: 100 }).listAll(),\n billow.products.list(),\n billow.coupons.list({ limit: 100 }).listAll(),\n ]);\n\n const featureBySlug = new Map(features.map((f) => [f.slug, f]));\n const productBySlug = new Map(products.map((p) => [p.slug, p]));\n const couponByCode = new Map(coupons.filter((c) => c.code).map((c) => [c.code as string, c]));\n\n return {\n features: (config.features ?? []).map((f) => planFeature(f, featureBySlug.get(f.slug))),\n products: (config.products ?? []).map((p) => planProduct(p, productBySlug.get(p.slug))),\n coupons: (config.coupons ?? []).map((c) => planCoupon(c, couponByCode.get(c.code))),\n };\n}\n\n/**\n * Apply a config: create everything missing (features → products → coupons, the\n * dependency order, since products reference feature slugs), and return what was\n * created plus any drift it found.\n *\n * Structural drift (prices, entitlements, name, ...) is never auto-applied - those edits are\n * version-bearing (ADR-0015: collected at renewal) and belong to a deliberate `products.update`.\n * With `applyMetadata: true`, product **metadata** drift IS written, because metadata lives on\n * the product row (not the version), so updating it is version-independent and cannot rewrite\n * billed history. This closes the write-side gap where a config's product metadata could only\n * be reported, never pushed. The write is a full-map replace (config is authoritative): a key\n * present on the server but absent from the config is cleared.\n */\nexport async function syncConfig(\n billow: Billow,\n config: BillowConfig,\n opts: { onProgress?: (message: string) => void; applyMetadata?: boolean } = {},\n): Promise<SyncResult> {\n const plan = await planConfig(billow, config);\n const progress = opts.onProgress ?? (() => {});\n\n // Create in dependency order — features → products → coupons — since products\n // reference feature slugs. Each kind is created before the next begins.\n const created = {\n features: await createMissing(\n plan.features,\n config.features ?? [],\n (f) => f.slug,\n \"feature\",\n (f) => billow.features.create(f),\n progress,\n ),\n products: await createMissing(\n plan.products,\n config.products ?? [],\n (p) => p.slug,\n \"product\",\n (p) => billow.products.create(p),\n progress,\n ),\n coupons: await createMissing(\n plan.coupons,\n config.coupons ?? [],\n (c) => c.code,\n \"coupon\",\n (c) => billow.coupons.create(c),\n progress,\n ),\n };\n\n // Version-independent metadata is the one field safe to auto-write (a product-row column,\n // not a version). Apply it before computing drift so a resolved item drops out of the report.\n const applied = { productMetadata: [] as string[] };\n if (opts.applyMetadata) {\n const isMetadataChange = (c: string) => c.startsWith(\"metadata\");\n const productDefBySlug = new Map((config.products ?? []).map((p) => [p.slug, p]));\n for (const item of plan.products) {\n if (item.action !== \"drift\" || !item.changes?.some(isMetadataChange)) continue;\n const def = productDefBySlug.get(item.id);\n if (!def) continue;\n await billow.products.update(item.id, { metadata: def.metadata ?? {} });\n applied.productMetadata.push(item.id);\n progress(`~ product ${item.id} (metadata applied)`);\n // Reflect the write in the plan: drop metadata from this item, and mark it unchanged if\n // it had no other drift, so the returned drift no longer reports what we just fixed.\n item.changes = item.changes.filter((c) => !isMetadataChange(c));\n if (item.changes.length === 0) item.action = \"unchanged\";\n }\n }\n\n const drift = [...plan.features, ...plan.products, ...plan.coupons].filter(\n (i) => i.action === \"drift\",\n );\n return { plan, created, drift, applied };\n}\n\n/**\n * Create the `action: \"create\"` items of one kind, matching each plan item back to\n * its config definition by id (a Map, not a scan), and return the ids created.\n */\nasync function createMissing<T>(\n items: SyncItem[],\n defs: T[],\n idOf: (def: T) => string,\n label: string,\n create: (def: T) => Promise<unknown>,\n progress: (message: string) => void,\n): Promise<string[]> {\n const byId = new Map(defs.map((d) => [idOf(d), d]));\n const created: string[] = [];\n for (const item of items) {\n if (item.action !== \"create\") continue;\n const def = byId.get(item.id);\n if (!def) continue;\n await create(def);\n created.push(item.id);\n progress(`+ ${label} ${item.id}`);\n }\n return created;\n}\n\n/** A one-line-per-item summary of a plan, for CLI output. */\nexport function formatPlan(plan: SyncPlan): string {\n const mark = { create: \"+\", unchanged: \"=\", drift: \"~\" } as const;\n const lines: string[] = [];\n for (const item of [...plan.features, ...plan.products, ...plan.coupons]) {\n const detail = item.changes?.length ? ` (${item.changes.join(\", \")})` : \"\";\n lines.push(`${mark[item.action]} ${item.kind} ${item.id}${detail}`);\n }\n return lines.join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Per-entity diffing\n// ---------------------------------------------------------------------------\n\nfunction planFeature(def: ConfigFeature, existing: Feature | undefined): SyncItem {\n if (!existing) return { kind: \"feature\", id: def.slug, action: \"create\" };\n const changes = diffFields([\n [\"name\", def.name, existing.name],\n [\"kind\", def.kind, existing.kind],\n // A metered feature with no explicit meter gets a `sum` meter server-side; normalize\n // both sides so an aggregation change is reported as drift, not \"unchanged\".\n [\n \"meter\",\n normMeter(def.kind, def.meter),\n normMeter(existing.kind, existing.meter ?? undefined),\n ],\n ]);\n return { kind: \"feature\", id: def.slug, action: changes.length ? \"drift\" : \"unchanged\", changes };\n}\n\nfunction planProduct(def: ConfigProduct, existing: Product | undefined): SyncItem {\n if (!existing) return { kind: \"product\", id: def.slug, action: \"create\" };\n const changes = diffFields([\n [\"name\", def.name, existing.name],\n [\"type\", def.type, existing.type],\n [\"isAddOn\", def.isAddOn ?? false, existing.isAddOn],\n [\"metadata\", normMeta(def.metadata), normMeta(existing.metadata)],\n ]);\n if (!sameSet(def.prices.map(normPrice), (existing.prices ?? []).map(normPrice))) {\n changes.push(\"prices\");\n }\n if (!sameSet((def.entitlements ?? []).map(normEnt), (existing.entitlements ?? []).map(normEnt))) {\n changes.push(\"entitlements\");\n }\n return { kind: \"product\", id: def.slug, action: changes.length ? \"drift\" : \"unchanged\", changes };\n}\n\nfunction planCoupon(def: ConfigCoupon, existing: Coupon | undefined): SyncItem {\n if (!existing) return { kind: \"coupon\", id: def.code, action: \"create\" };\n const changes = diffFields([\n [\"kind\", def.kind, existing.kind],\n [\"value\", def.value, existing.value],\n [\"currency\", def.currency?.toUpperCase() ?? null, existing.currency],\n [\"duration\", def.duration, existing.duration],\n [\"durationCycles\", def.durationCycles ?? null, existing.durationCycles],\n [\"maxRedemptions\", def.maxRedemptions ?? null, existing.maxRedemptions],\n [\"expiresAt\", epochOrNull(def.expiresAt), epochOrNull(existing.expiresAt)],\n ]);\n if (!sameSet(def.productRestrictions ?? [], existing.productRestrictions ?? [])) {\n changes.push(\"productRestrictions\");\n }\n return { kind: \"coupon\", id: def.code, action: changes.length ? \"drift\" : \"unchanged\", changes };\n}\n\n// ---------------------------------------------------------------------------\n// Normalization helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Canonicalize a feature's meter for drift comparison. A boolean feature has no meter\n * (null); a metered feature with no explicit config gets the server's `sum` default, so\n * an omitted meter and an explicit `sum` meter compare equal. Filter objects are reshaped\n * to a fixed key order (a jsonb round-trip can reorder keys) while list order is preserved.\n */\n/**\n * Canonicalize a product's metadata map for drift comparison: sort keys so a mere\n * key-order difference (jsonb round-trips keys in insertion order) isn't reported as drift.\n * An omitted map and an empty map compare equal.\n */\nfunction normMeta(metadata: Record<string, string> | undefined): string {\n const entries = Object.entries(metadata ?? {}).sort((a, b) =>\n a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0,\n );\n return JSON.stringify(entries);\n}\n\nfunction normMeter(kind: FeatureKind, meter: MeterConfig | undefined): string | null {\n if (kind !== \"metered\") return null;\n return JSON.stringify({\n aggregation: meter?.aggregation ?? \"sum\",\n eventProperty: meter?.eventProperty ?? null,\n // The filter list and each filter's values are unordered sets — sort both so a mere\n // reordering isn't reported as drift (matches the server's no-op semantics).\n filters: (meter?.filters ?? [])\n .map((f) => ({ key: f.key, values: [...f.values].sort() }))\n // Total order: by key, then by (already-sorted) values, so same-key duplicate\n // filters canonicalize too rather than keeping input order.\n .sort((a, b) => {\n if (a.key !== b.key) return a.key < b.key ? -1 : 1;\n const av = JSON.stringify(a.values);\n const bv = JSON.stringify(b.values);\n return av < bv ? -1 : av > bv ? 1 : 0;\n }),\n roundingMode: meter?.roundingMode ?? \"none\",\n roundingScale: meter?.roundingScale ?? null,\n // Reset anchoring (plan 07). Server omits these for cycle meters, so default to cycle/null\n // on both sides — an omitted anchor and an explicit `cycle` compare equal (no false drift).\n resetAnchor: meter?.resetAnchor ?? \"cycle\",\n resetTz: meter?.resetTz ?? null,\n });\n}\n\nfunction normPrice(p: {\n kind: string;\n interval?: string | null;\n intervalCount?: number;\n amount: number;\n currency: string;\n meterFeature?: string | null;\n usageModel?: string | null;\n usageTiers?: unknown;\n packageSize?: number | null;\n rateBps?: number | null;\n taxRateBps?: number | null;\n taxBehavior?: string | null;\n}): string {\n return JSON.stringify({\n kind: p.kind,\n interval: p.interval ?? null,\n intervalCount: p.intervalCount ?? 1,\n amount: p.amount,\n currency: p.currency.toUpperCase(),\n meterFeature: p.meterFeature ?? null,\n // Usage prices default to graduated server-side; normalize so a model change\n // (or package/percentage param change) is detected as drift, not \"unchanged\".\n usageModel: p.kind === \"usage\" ? (p.usageModel ?? \"graduated\") : null,\n usageTiers: canonTiers(p.usageTiers),\n packageSize: p.packageSize ?? null,\n rateBps: p.rateBps ?? null,\n // Tax overrides (Phase E) — null = inherit org default; included so a change\n // is detected as drift.\n taxRateBps: p.taxRateBps ?? null,\n taxBehavior: p.taxBehavior ?? null,\n });\n}\n\n/**\n * Canonicalize usage tiers to a fixed key order before comparison. A jsonb\n * round-trip can reorder object keys, so raw `JSON.stringify` would report drift\n * on an unchanged tier set — normalize each tier to `{ upTo, unitAmount, flatAmount }`.\n */\nfunction canonTiers(\n tiers: unknown,\n): { upTo: unknown; unitAmount: unknown; flatAmount: unknown }[] | null {\n if (!Array.isArray(tiers)) return null;\n return tiers.map((t) => {\n const tier = (t ?? {}) as { upTo?: unknown; unitAmount?: unknown; flatAmount?: unknown };\n return {\n upTo: tier.upTo ?? null,\n unitAmount: tier.unitAmount ?? null,\n flatAmount: tier.flatAmount ?? null,\n };\n });\n}\n\nfunction normEnt(e: {\n feature: string;\n allowance?: number | null;\n resetInterval?: string | null;\n rollover?: boolean;\n}): string {\n return JSON.stringify({\n feature: e.feature,\n allowance: e.allowance ?? null,\n resetInterval: e.resetInterval ?? null,\n rollover: e.rollover ?? false,\n });\n}\n\n/** Field-by-field equality, returning `\"field: a → b\"` for each mismatch. */\nfunction diffFields(fields: [string, unknown, unknown][]): string[] {\n const changes: string[] = [];\n for (const [name, want, have] of fields) {\n if (want !== have) changes.push(`${name}: ${fmt(have)} → ${fmt(want)}`);\n }\n return changes;\n}\n\n/** Order-insensitive equality of two string lists (compared as multisets). */\nfunction sameSet(a: string[], b: string[]): boolean {\n if (a.length !== b.length) return false;\n const sortedA = [...a].sort();\n const sortedB = [...b].sort();\n return sortedA.every((v, i) => v === sortedB[i]);\n}\n\nfunction epochOrNull(value: string | null | undefined): number | null {\n return value ? new Date(value).getTime() : null;\n}\n\nfunction fmt(value: unknown): string {\n if (value === null || value === undefined) return \"∅\";\n return String(value);\n}\n"]}
|