@volter/twin-polar 0.1.2 → 2.0.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/README.md +2 -2
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +26 -0
- package/dist/src/index.d.ts +9 -0
- package/dist/src/index.js +55 -0
- package/dist/src/polar-budget.d.ts +83 -0
- package/dist/src/polar-budget.js +413 -0
- package/dist/src/polar-capabilities.d.ts +4 -0
- package/dist/src/polar-capabilities.js +389 -0
- package/dist/src/polar-conformance.d.ts +7 -0
- package/dist/src/polar-conformance.js +25 -0
- package/dist/src/polar-connector.d.ts +113 -0
- package/dist/src/polar-connector.js +146 -0
- package/dist/src/polar-server.d.ts +14 -0
- package/dist/src/polar-server.js +52 -0
- package/dist/src/polar-twin.d.ts +24 -0
- package/dist/src/polar-twin.js +941 -0
- package/package.json +16 -9
- package/src/cli.ts +6 -6
- package/src/index.ts +22 -6
- package/src/polar-budget.ts +22 -6
- package/src/polar-capabilities.ts +54 -19
- package/src/polar-connector.ts +43 -26
- package/src/polar-server.ts +28 -4
- package/src/polar-twin.ts +159 -49
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
// Polar capability manifest — the EXPECTED REAL-PRODUCT SURFACE (the target), authored
|
|
2
|
+
// top-down from what Polar (polar.sh) actually does — NOT from what this twin has built. The
|
|
3
|
+
// honest denominator: most entries start as `todo`. `verify()` (required to count as done) is
|
|
4
|
+
// ground truth and drives the KERNEL-BACKED `handlePolarTwinRequest` against a FRESH temp root
|
|
5
|
+
// (no spawned mock; no in-memory side-store) — create → read → assert VALUES + a negative 4xx
|
|
6
|
+
// in Polar's error shape, so every claim is failable.
|
|
7
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { checkCapabilities, verifyBoundary } from '@volter/world-tooling';
|
|
11
|
+
import { checkPolarConformance } from "./polar-conformance.js";
|
|
12
|
+
import { syncPolarFromReal, performPolarAction } from "./polar-connector.js";
|
|
13
|
+
import { handlePolarTwinRequest } from "./polar-twin.js";
|
|
14
|
+
const done = (id, area, title, dimension, tier, verify) => ({ id, area, title, dimension, tier, expected: 'done', verify });
|
|
15
|
+
const todo = (id, area, title, dimension, tier) => ({ id, area, title, dimension, tier, expected: 'todo' });
|
|
16
|
+
/** Run a sequence of real Polar requests against an isolated kernel root; return all responses. */
|
|
17
|
+
async function withRoot(steps) {
|
|
18
|
+
const root = mkdtempSync(join(tmpdir(), 'polar-cap-'));
|
|
19
|
+
const h = (s) => handlePolarTwinRequest({ method: s.m, path: s.p, body: s.b === undefined ? undefined : JSON.stringify(s.b), root });
|
|
20
|
+
try {
|
|
21
|
+
return await verifyBoundary('polar.withRoot', () => steps(h));
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
rmSync(root, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const ok = (r) => r.status >= 200 && r.status < 300;
|
|
28
|
+
const id = (r) => r.body?.id;
|
|
29
|
+
const field = (r, k) => r.body?.[k];
|
|
30
|
+
const items = (r) => r.body?.items;
|
|
31
|
+
/** A Polar ResourceNotFound 404 (the real error envelope). */
|
|
32
|
+
const isNotFound = (r) => r.status === 404 && r.body?.error === 'ResourceNotFound'; // Polar's envelope: { error, detail }
|
|
33
|
+
/** A FastAPI/Polar 422 validation envelope. */
|
|
34
|
+
const isValidation = (r) => r.status === 422 && Array.isArray(r.body?.detail);
|
|
35
|
+
export const POLAR_CAPABILITIES = [
|
|
36
|
+
// ── Customers ─────────────────────────────────────────────────────────────────────
|
|
37
|
+
done('polar.customers.create', 'customers', 'Create customer with email/name/external_id', 'api', 'core', () => withRoot(async (h) => {
|
|
38
|
+
const c = await h({ m: 'POST', p: '/v1/customers/', b: { email: 'a@example.test', name: 'A', external_id: 'ext-1' } });
|
|
39
|
+
return c.status === 201 && typeof id(c) === 'string' && field(c, 'email') === 'a@example.test' && field(c, 'name') === 'A' && field(c, 'external_id') === 'ext-1' && field(c, 'organization_id') != null;
|
|
40
|
+
})),
|
|
41
|
+
done('polar.customers.create_requires_email', 'customers', 'Creating an individual customer without email → 422', 'api', 'core', () => withRoot(async (h) => {
|
|
42
|
+
const c = await h({ m: 'POST', p: '/v1/customers/', b: { name: 'No Email' } });
|
|
43
|
+
return isValidation(c);
|
|
44
|
+
})),
|
|
45
|
+
done('polar.customers.duplicate_email', 'customers', 'Duplicate email → 422 (not a fabricated success)', 'api', 'common', () => withRoot(async (h) => {
|
|
46
|
+
await h({ m: 'POST', p: '/v1/customers/', b: { email: 'dup@example.test' } });
|
|
47
|
+
const c = await h({ m: 'POST', p: '/v1/customers/', b: { email: 'dup@example.test' } });
|
|
48
|
+
return c.status === 422;
|
|
49
|
+
})),
|
|
50
|
+
done('polar.customers.get', 'customers', 'Retrieve a customer by id; unknown id → 404', 'api', 'core', () => withRoot(async (h) => {
|
|
51
|
+
const c = await h({ m: 'POST', p: '/v1/customers/', b: { email: 'get@example.test' } });
|
|
52
|
+
const g = await h({ m: 'GET', p: `/v1/customers/${id(c)}` });
|
|
53
|
+
const miss = await h({ m: 'GET', p: '/v1/customers/00000000-0000-4000-8000-000000000999' });
|
|
54
|
+
return ok(g) && id(g) === id(c) && field(g, 'email') === 'get@example.test' && isNotFound(miss);
|
|
55
|
+
})),
|
|
56
|
+
done('polar.customers.list_filter_email', 'customers', 'List customers and filter by email', 'api', 'core', () => withRoot(async (h) => {
|
|
57
|
+
await h({ m: 'POST', p: '/v1/customers/', b: { email: 'list@example.test' } });
|
|
58
|
+
await h({ m: 'POST', p: '/v1/customers/', b: { email: 'other@example.test' } });
|
|
59
|
+
const r = await h({ m: 'GET', p: '/v1/customers/?email=list@example.test' });
|
|
60
|
+
return ok(r) && items(r).length === 1 && items(r)[0].email === 'list@example.test' && field(r, 'pagination').total_count === 1;
|
|
61
|
+
})),
|
|
62
|
+
done('polar.customers.update', 'customers', 'Update (PATCH) customer name/metadata; unknown → 404', 'api', 'common', () => withRoot(async (h) => {
|
|
63
|
+
const c = await h({ m: 'POST', p: '/v1/customers/', b: { email: 'patch@example.test', name: 'Old' } });
|
|
64
|
+
const u = await h({ m: 'PATCH', p: `/v1/customers/${id(c)}`, b: { name: 'New', metadata: { tier: 'pro' } } });
|
|
65
|
+
const miss = await h({ m: 'PATCH', p: '/v1/customers/nope', b: { name: 'x' } });
|
|
66
|
+
return ok(u) && field(u, 'name') === 'New' && field(u, 'metadata').tier === 'pro' && isNotFound(miss);
|
|
67
|
+
})),
|
|
68
|
+
done('polar.customers.delete', 'customers', 'Delete (soft) a customer → 204, then 404 on read', 'api', 'common', () => withRoot(async (h) => {
|
|
69
|
+
const c = await h({ m: 'POST', p: '/v1/customers/', b: { email: 'del@example.test' } });
|
|
70
|
+
const d = await h({ m: 'DELETE', p: `/v1/customers/${id(c)}` });
|
|
71
|
+
const g = await h({ m: 'GET', p: `/v1/customers/${id(c)}` });
|
|
72
|
+
return d.status === 204 && isNotFound(g);
|
|
73
|
+
})),
|
|
74
|
+
done('polar.customers.state_meter_balance', 'customers', 'Customer state returns active meter balance shape', 'api', 'core', () => withRoot(async (h) => {
|
|
75
|
+
const c = await h({ m: 'POST', p: '/v1/customers/', b: { email: 'state@example.test' } });
|
|
76
|
+
const s = await h({ m: 'GET', p: `/v1/customers/${id(c)}/state` });
|
|
77
|
+
const meters = field(s, 'active_meters');
|
|
78
|
+
return ok(s) && Array.isArray(meters) && typeof meters[0]?.balance === 'number' && Array.isArray(field(s, 'active_subscriptions'));
|
|
79
|
+
})),
|
|
80
|
+
// ── Products ──────────────────────────────────────────────────────────────────────
|
|
81
|
+
done('polar.products.create', 'products', 'Create a recurring product; retrieve round-trips', 'api', 'core', () => withRoot(async (h) => {
|
|
82
|
+
const p = await h({ m: 'POST', p: '/v1/products/', b: { name: 'Pro', recurring_interval: 'month', prices: [{ price_amount: 1500, price_currency: 'usd' }] } });
|
|
83
|
+
const g = await h({ m: 'GET', p: `/v1/products/${id(p)}` });
|
|
84
|
+
return p.status === 201 && field(p, 'name') === 'Pro' && field(p, 'is_recurring') === true && ok(g) && field(g, 'prices').length === 1;
|
|
85
|
+
})),
|
|
86
|
+
done('polar.products.create_requires_name', 'products', 'Create product without name → 422', 'api', 'common', () => withRoot(async (h) => isValidation(await h({ m: 'POST', p: '/v1/products/', b: {} })))),
|
|
87
|
+
done('polar.products.list_and_archive', 'products', 'List products + archive via PATCH is_archived', 'api', 'common', () => withRoot(async (h) => {
|
|
88
|
+
const p = await h({ m: 'POST', p: '/v1/products/', b: { name: 'Arch' } });
|
|
89
|
+
const u = await h({ m: 'PATCH', p: `/v1/products/${id(p)}`, b: { is_archived: true } });
|
|
90
|
+
const l = await h({ m: 'GET', p: '/v1/products/?is_archived=true' });
|
|
91
|
+
return field(u, 'is_archived') === true && items(l).some((x) => x.id === id(p));
|
|
92
|
+
})),
|
|
93
|
+
// ── Checkouts + confirm → subscription/order ────────────────────────────────────────
|
|
94
|
+
done('polar.checkouts.create', 'checkouts', 'Create checkout with client secret + hosted url', 'api', 'core', () => withRoot(async (h) => {
|
|
95
|
+
const prod = await h({ m: 'POST', p: '/v1/products/', b: { name: 'CO', recurring_interval: 'month', prices: [{ price_amount: 2000 }] } });
|
|
96
|
+
const c = await h({ m: 'POST', p: '/v1/checkouts/', b: { products: [id(prod)] } });
|
|
97
|
+
return c.status === 201 && String(field(c, 'client_secret')).startsWith('polar_checkout_') && String(field(c, 'url')).includes('/checkout/') && field(c, 'status') === 'open' && field(c, 'amount') === 2000;
|
|
98
|
+
})),
|
|
99
|
+
done('polar.checkouts.requires_product', 'checkouts', 'Checkout without products → 422', 'api', 'common', () => withRoot(async (h) => isValidation(await h({ m: 'POST', p: '/v1/checkouts/', b: {} })))),
|
|
100
|
+
done('polar.checkouts.confirm_creates_subscription', 'checkouts', 'Confirm a recurring checkout → active subscription + paid order + customer', 'api', 'core', () => withRoot(async (h) => {
|
|
101
|
+
const prod = await h({ m: 'POST', p: '/v1/products/', b: { name: 'Sub', recurring_interval: 'month', prices: [{ price_amount: 1000 }] } });
|
|
102
|
+
const c = await h({ m: 'POST', p: '/v1/checkouts/', b: { products: [id(prod)], customer_email: 'buyer@example.test' } });
|
|
103
|
+
const conf = await h({ m: 'POST', p: `/v1/checkouts/${id(c)}/confirm`, b: { customer_email: 'buyer@example.test' } });
|
|
104
|
+
if (!ok(conf) || field(conf, 'status') !== 'confirmed' || !field(conf, 'subscription_id'))
|
|
105
|
+
return false;
|
|
106
|
+
const subs = await h({ m: 'GET', p: '/v1/subscriptions/' });
|
|
107
|
+
const orders = await h({ m: 'GET', p: '/v1/orders/' });
|
|
108
|
+
return items(subs).length === 1 && items(subs)[0].status === 'active' && items(orders).length === 1 && items(orders)[0].paid === true;
|
|
109
|
+
})),
|
|
110
|
+
done('polar.checkouts.confirm_twice_rejected', 'checkouts', 'Confirming an already-confirmed checkout → 403', 'api', 'common', () => withRoot(async (h) => {
|
|
111
|
+
const prod = await h({ m: 'POST', p: '/v1/products/', b: { name: 'Once', recurring_interval: 'month', prices: [{ price_amount: 500 }] } });
|
|
112
|
+
const c = await h({ m: 'POST', p: '/v1/checkouts/', b: { products: [id(prod)], customer_email: 'x@example.test' } });
|
|
113
|
+
await h({ m: 'POST', p: `/v1/checkouts/${id(c)}/confirm`, b: { customer_email: 'x@example.test' } });
|
|
114
|
+
const again = await h({ m: 'POST', p: `/v1/checkouts/${id(c)}/confirm`, b: {} });
|
|
115
|
+
return again.status === 403;
|
|
116
|
+
})),
|
|
117
|
+
// ── Subscriptions ───────────────────────────────────────────────────────────────────
|
|
118
|
+
done('polar.subscriptions.list_get', 'subscriptions', 'List + retrieve subscriptions; unknown → 404', 'api', 'core', () => withRoot(async (h) => {
|
|
119
|
+
const prod = await h({ m: 'POST', p: '/v1/products/', b: { name: 'S', recurring_interval: 'month', prices: [{ price_amount: 100 }] } });
|
|
120
|
+
const c = await h({ m: 'POST', p: '/v1/checkouts/', b: { products: [id(prod)], customer_email: 's@example.test' } });
|
|
121
|
+
const conf = await h({ m: 'POST', p: `/v1/checkouts/${id(c)}/confirm`, b: { customer_email: 's@example.test' } });
|
|
122
|
+
const subId = field(conf, 'subscription_id');
|
|
123
|
+
const g = await h({ m: 'GET', p: `/v1/subscriptions/${subId}` });
|
|
124
|
+
const miss = await h({ m: 'GET', p: '/v1/subscriptions/nope' });
|
|
125
|
+
return ok(g) && id(g) === subId && field(g, 'customer') != null && isNotFound(miss);
|
|
126
|
+
})),
|
|
127
|
+
done('polar.subscriptions.patch_cancel_at_period_end', 'subscriptions', 'PATCH cancel_at_period_end flips flag + records canceled_at', 'api', 'common', () => withRoot(async (h) => {
|
|
128
|
+
const prod = await h({ m: 'POST', p: '/v1/products/', b: { name: 'P', recurring_interval: 'month', prices: [{ price_amount: 100 }] } });
|
|
129
|
+
const c = await h({ m: 'POST', p: '/v1/checkouts/', b: { products: [id(prod)], customer_email: 'p@example.test' } });
|
|
130
|
+
const conf = await h({ m: 'POST', p: `/v1/checkouts/${id(c)}/confirm`, b: { customer_email: 'p@example.test' } });
|
|
131
|
+
const subId = field(conf, 'subscription_id');
|
|
132
|
+
const u = await h({ m: 'PATCH', p: `/v1/subscriptions/${subId}`, b: { cancel_at_period_end: true } });
|
|
133
|
+
return ok(u) && field(u, 'cancel_at_period_end') === true && field(u, 'canceled_at') != null && field(u, 'status') === 'active';
|
|
134
|
+
})),
|
|
135
|
+
done('polar.subscriptions.cancel', 'subscriptions', 'DELETE subscription → status canceled + ended_at', 'api', 'common', () => withRoot(async (h) => {
|
|
136
|
+
const prod = await h({ m: 'POST', p: '/v1/products/', b: { name: 'C', recurring_interval: 'month', prices: [{ price_amount: 100 }] } });
|
|
137
|
+
const c = await h({ m: 'POST', p: '/v1/checkouts/', b: { products: [id(prod)], customer_email: 'c@example.test' } });
|
|
138
|
+
const conf = await h({ m: 'POST', p: `/v1/checkouts/${id(c)}/confirm`, b: { customer_email: 'c@example.test' } });
|
|
139
|
+
const subId = field(conf, 'subscription_id');
|
|
140
|
+
const d = await h({ m: 'DELETE', p: `/v1/subscriptions/${subId}` });
|
|
141
|
+
return ok(d) && field(d, 'status') === 'canceled' && field(d, 'ended_at') != null;
|
|
142
|
+
})),
|
|
143
|
+
// ── Orders ──────────────────────────────────────────────────────────────────────────
|
|
144
|
+
done('polar.orders.list_filter', 'orders', 'List orders + filter by customer_id', 'api', 'common', () => withRoot(async (h) => {
|
|
145
|
+
const prod = await h({ m: 'POST', p: '/v1/products/', b: { name: 'O', prices: [{ price_amount: 700 }] } });
|
|
146
|
+
const c = await h({ m: 'POST', p: '/v1/checkouts/', b: { products: [id(prod)], customer_email: 'o@example.test' } });
|
|
147
|
+
const conf = await h({ m: 'POST', p: `/v1/checkouts/${id(c)}/confirm`, b: { customer_email: 'o@example.test' } });
|
|
148
|
+
const custId = field(conf, 'customer_id');
|
|
149
|
+
const l = await h({ m: 'GET', p: `/v1/orders/?customer_id=${custId}` });
|
|
150
|
+
return items(l).length === 1 && items(l)[0].customer_id === custId;
|
|
151
|
+
})),
|
|
152
|
+
done('polar.orders.refund', 'orders', 'Refund an order → status refunded + refunded_amount', 'api', 'common', () => withRoot(async (h) => {
|
|
153
|
+
const prod = await h({ m: 'POST', p: '/v1/products/', b: { name: 'R', prices: [{ price_amount: 900 }] } });
|
|
154
|
+
const c = await h({ m: 'POST', p: '/v1/checkouts/', b: { products: [id(prod)], customer_email: 'r@example.test' } });
|
|
155
|
+
const conf = await h({ m: 'POST', p: `/v1/checkouts/${id(c)}/confirm`, b: { customer_email: 'r@example.test' } });
|
|
156
|
+
const ordList = await h({ m: 'GET', p: '/v1/orders/' });
|
|
157
|
+
const orderId = items(ordList)[0].id;
|
|
158
|
+
const ref = await h({ m: 'POST', p: `/v1/orders/${orderId}/refund`, b: { amount: 900 } });
|
|
159
|
+
const miss = await h({ m: 'POST', p: '/v1/orders/nope/refund', b: {} });
|
|
160
|
+
return ok(ref) && field(ref, 'status') === 'refunded' && field(ref, 'refunded_amount') === 900 && field(ref, 'paid') === false && isNotFound(miss) && Boolean(conf);
|
|
161
|
+
})),
|
|
162
|
+
// ── Benefits / Discounts / Meters ────────────────────────────────────────────────────
|
|
163
|
+
done('polar.benefits.crud', 'benefits', 'Create/list/delete a benefit', 'api', 'common', () => withRoot(async (h) => {
|
|
164
|
+
const b = await h({ m: 'POST', p: '/v1/benefits/', b: { type: 'custom', description: 'VIP' } });
|
|
165
|
+
const g = await h({ m: 'GET', p: `/v1/benefits/${id(b)}` });
|
|
166
|
+
const d = await h({ m: 'DELETE', p: `/v1/benefits/${id(b)}` });
|
|
167
|
+
const after = await h({ m: 'GET', p: `/v1/benefits/${id(b)}` });
|
|
168
|
+
return b.status === 201 && field(g, 'description') === 'VIP' && d.status === 204 && isNotFound(after);
|
|
169
|
+
})),
|
|
170
|
+
done('polar.discounts.crud', 'discounts', 'Create/update/list discounts; requires name+type', 'api', 'common', () => withRoot(async (h) => {
|
|
171
|
+
const bad = await h({ m: 'POST', p: '/v1/discounts/', b: { name: 'NoType' } });
|
|
172
|
+
const d = await h({ m: 'POST', p: '/v1/discounts/', b: { name: 'Launch', type: 'percentage', basis_points: 2000 } });
|
|
173
|
+
const u = await h({ m: 'PATCH', p: `/v1/discounts/${id(d)}`, b: { basis_points: 5000 } });
|
|
174
|
+
return isValidation(bad) && d.status === 201 && field(d, 'type') === 'percentage' && field(u, 'basis_points') === 5000;
|
|
175
|
+
})),
|
|
176
|
+
done('polar.meters.crud', 'meters', 'Create + retrieve a usage meter', 'api', 'common', () => withRoot(async (h) => {
|
|
177
|
+
const m = await h({ m: 'POST', p: '/v1/meters/', b: { name: 'tokens', aggregation: { func: 'sum', property: 'amount' } } });
|
|
178
|
+
const g = await h({ m: 'GET', p: `/v1/meters/${id(m)}` });
|
|
179
|
+
return m.status === 201 && field(g, 'name') === 'tokens' && field(g, 'aggregation').func === 'sum';
|
|
180
|
+
})),
|
|
181
|
+
// ── Events ────────────────────────────────────────────────────────────────────────
|
|
182
|
+
done('polar.events.ingest', 'events', 'Ingest usage events → inserted count', 'api', 'core', () => withRoot(async (h) => {
|
|
183
|
+
const r = await h({ m: 'POST', p: '/v1/events/ingest', b: { events: [{ name: 'tokens', metadata: { cost_usd: 1 } }, { name: 'tokens' }] } });
|
|
184
|
+
return ok(r) && field(r, 'inserted') === 2 && field(r, 'duplicates') === 0;
|
|
185
|
+
})),
|
|
186
|
+
done('polar.events.ingest_validates_name', 'events', 'Ingest with a nameless event → 422', 'api', 'common', () => withRoot(async (h) => isValidation(await h({ m: 'POST', p: '/v1/events/ingest', b: { events: [{ metadata: {} }] } })))),
|
|
187
|
+
done('polar.events.list_filter', 'events', 'List events + filter by customer_id', 'api', 'common', () => withRoot(async (h) => {
|
|
188
|
+
const c = await h({ m: 'POST', p: '/v1/customers/', b: { email: 'ev@example.test' } });
|
|
189
|
+
await h({ m: 'POST', p: '/v1/events/ingest', b: { events: [{ name: 'used', customer_id: id(c) }] } });
|
|
190
|
+
await h({ m: 'POST', p: '/v1/events/ingest', b: { events: [{ name: 'other' }] } });
|
|
191
|
+
const l = await h({ m: 'GET', p: `/v1/events/?customer_id=${id(c)}` });
|
|
192
|
+
return items(l).length === 1 && items(l)[0].name === 'used';
|
|
193
|
+
})),
|
|
194
|
+
done('polar.events.balance_reflects_usage', 'events', 'Customer state meter balance nets credits − consumption', 'api', 'common', () => withRoot(async (h) => {
|
|
195
|
+
const c = await h({ m: 'POST', p: '/v1/customers/', b: { email: 'bal2@example.test' } });
|
|
196
|
+
await h({ m: 'POST', p: '/v1/events/ingest', b: { events: [{ name: 'credit', customer_id: id(c), metadata: { cost_usd: -5 } }, { name: 'use', customer_id: id(c), metadata: { cost_usd: 2 } }] } });
|
|
197
|
+
const s = await h({ m: 'GET', p: `/v1/customers/${id(c)}/state` });
|
|
198
|
+
return field(s, 'active_meters')[0].balance === 3;
|
|
199
|
+
})),
|
|
200
|
+
// ── The customer by YOUR id, meters as Polar keeps them (2026-09-07, the platform's billing) ──
|
|
201
|
+
done('polar.customers.get_external_state', 'customers', 'GET /customers/external/{id} and its /state: the customer by external id; unknown → 404', 'api', 'core', () => withRoot(async (h) => {
|
|
202
|
+
await h({ m: 'POST', p: '/v1/customers/', b: { email: 'ext@example.test', external_id: 'org_1' } });
|
|
203
|
+
const g = await h({ m: 'GET', p: '/v1/customers/external/org_1' });
|
|
204
|
+
const s = await h({ m: 'GET', p: '/v1/customers/external/org_1/state' });
|
|
205
|
+
const miss = await h({ m: 'GET', p: '/v1/customers/external/nobody/state' });
|
|
206
|
+
return ok(g) && field(g, 'external_id') === 'org_1' && ok(s) && Array.isArray(field(s, 'active_subscriptions')) && isNotFound(miss);
|
|
207
|
+
})),
|
|
208
|
+
done('polar.checkouts.external_customer_id', 'checkouts', 'A checkout created with external_customer_id confirms onto the customer that id names (created if new)', 'api', 'core', () => withRoot(async (h) => {
|
|
209
|
+
const p = await h({ m: 'POST', p: '/v1/products/', b: { name: 'Team', recurring_interval: 'month', prices: [{ price_amount: 2000, price_currency: 'usd' }] } });
|
|
210
|
+
const c = await h({ m: 'POST', p: '/v1/checkouts/', b: { products: [id(p)], external_customer_id: 'org_42', customer_email: 'pay@example.test' } });
|
|
211
|
+
const done = await h({ m: 'POST', p: `/v1/checkouts/${id(c)}/confirm`, b: {} });
|
|
212
|
+
const s = await h({ m: 'GET', p: '/v1/customers/external/org_42/state' });
|
|
213
|
+
const subs = field(s, 'active_subscriptions');
|
|
214
|
+
return c.status === 201 && ok(done) && ok(s) && subs.length === 1 && subs[0].status === 'active';
|
|
215
|
+
})),
|
|
216
|
+
done('polar.events.ingest_dedupes_external_id', 'events', 'Ingest counts a repeated external_id as a duplicate and resolves external_customer_id to the customer', 'api', 'core', () => withRoot(async (h) => {
|
|
217
|
+
const first = await h({ m: 'POST', p: '/v1/events/ingest', b: { events: [{ name: 'world.hour', external_customer_id: 'org_7', external_id: 'org_7:h:1', metadata: { units: 1 } }] } });
|
|
218
|
+
const again = await h({ m: 'POST', p: '/v1/events/ingest', b: { events: [{ name: 'world.hour', external_customer_id: 'org_7', external_id: 'org_7:h:1', metadata: { units: 1 } }] } });
|
|
219
|
+
const list = await h({ m: 'GET', p: '/v1/events/?external_customer_id=org_7' });
|
|
220
|
+
const cust = await h({ m: 'GET', p: '/v1/customers/external/org_7' });
|
|
221
|
+
return field(first, 'inserted') === 1 && field(again, 'duplicates') === 1 && field(again, 'inserted') === 0 && field(list, 'items').length === 1 && ok(cust);
|
|
222
|
+
})),
|
|
223
|
+
done('polar.customers.state_per_meter_aggregation', 'customers', 'With meters defined, state carries one entry per meter aggregated by its filter and function', 'api', 'core', () => withRoot(async (h) => {
|
|
224
|
+
const m = await h({ m: 'POST', p: '/v1/meters/', b: { name: 'world_hours', filter: { conjunction: 'and', clauses: [{ property: 'name', operator: 'eq', value: 'world.hour' }] }, aggregation: { func: 'sum', property: 'units' } } });
|
|
225
|
+
await h({ m: 'POST', p: '/v1/meters/', b: { name: 'pushes', filter: { conjunction: 'and', clauses: [{ property: 'name', operator: 'eq', value: 'world.push' }] }, aggregation: { func: 'count' } } });
|
|
226
|
+
await h({ m: 'POST', p: '/v1/events/ingest', b: { events: [
|
|
227
|
+
{ name: 'world.hour', external_customer_id: 'org_9', external_id: 'a', metadata: { units: 2 } },
|
|
228
|
+
{ name: 'world.hour', external_customer_id: 'org_9', external_id: 'b', metadata: { units: 3 } },
|
|
229
|
+
{ name: 'world.push', external_customer_id: 'org_9', external_id: 'c' },
|
|
230
|
+
] } });
|
|
231
|
+
const s = await h({ m: 'GET', p: '/v1/customers/external/org_9/state' });
|
|
232
|
+
const meters = field(s, 'active_meters');
|
|
233
|
+
const hours = meters.find((x) => x.meter_id === id(m));
|
|
234
|
+
const pushes = meters.find((x) => x.meter_id !== id(m));
|
|
235
|
+
return meters.length === 2 && hours?.consumed_units === 5 && pushes?.consumed_units === 1;
|
|
236
|
+
})),
|
|
237
|
+
// ── Customer sessions ───────────────────────────────────────────────────────────────
|
|
238
|
+
done('polar.customer_sessions.create', 'customer-sessions', 'Create a customer portal session; unknown customer → 404', 'api', 'common', () => withRoot(async (h) => {
|
|
239
|
+
const c = await h({ m: 'POST', p: '/v1/customers/', b: { email: 'cs@example.test' } });
|
|
240
|
+
const s = await h({ m: 'POST', p: '/v1/customer-sessions/', b: { customer_id: id(c) } });
|
|
241
|
+
const miss = await h({ m: 'POST', p: '/v1/customer-sessions/', b: { customer_id: 'nope' } });
|
|
242
|
+
return s.status === 201 && String(field(s, 'token')).startsWith('polar_cst_') && String(field(s, 'customer_portal_url')).includes('customer_session_token') && isNotFound(miss);
|
|
243
|
+
})),
|
|
244
|
+
// ── Webhook endpoints ───────────────────────────────────────────────────────────────
|
|
245
|
+
done('polar.webhooks.endpoints_crud', 'webhooks', 'Create/list/delete webhook endpoints; create requires url', 'api', 'core', () => withRoot(async (h) => {
|
|
246
|
+
const bad = await h({ m: 'POST', p: '/v1/webhooks/endpoints/', b: {} });
|
|
247
|
+
const w = await h({ m: 'POST', p: '/v1/webhooks/endpoints/', b: { url: 'https://app.test/hook', events: ['order.created'] } });
|
|
248
|
+
const l = await h({ m: 'GET', p: '/v1/webhooks/endpoints/' });
|
|
249
|
+
const d = await h({ m: 'DELETE', p: `/v1/webhooks/endpoints/${id(w)}` });
|
|
250
|
+
return isValidation(bad) && w.status === 201 && items(l).length === 1 && d.status === 204;
|
|
251
|
+
})),
|
|
252
|
+
// ── Safety / honesty ────────────────────────────────────────────────────────────────
|
|
253
|
+
done('polar.read_only', 'safety', 'Read-only mode rejects writes with 405', 'api', 'core', () => withRoot(async () => {
|
|
254
|
+
const root = mkdtempSync(join(tmpdir(), 'polar-ro-'));
|
|
255
|
+
try {
|
|
256
|
+
const r = await handlePolarTwinRequest({ method: 'POST', path: '/v1/customers/', body: JSON.stringify({ email: 'ro@example.test' }), root, readOnly: true });
|
|
257
|
+
const g = await handlePolarTwinRequest({ method: 'GET', path: '/v1/customers/', root, readOnly: true });
|
|
258
|
+
return r.status === 405 && g.status === 200;
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
rmSync(root, { recursive: true, force: true });
|
|
262
|
+
}
|
|
263
|
+
})),
|
|
264
|
+
done('polar.errors.unmodeled_route', 'errors', 'Unmodeled route → ResourceNotFound (never fabricated success)', 'api', 'core', () => withRoot(async (h) => {
|
|
265
|
+
const r = await h({ m: 'POST', p: '/v1/totally-made-up/', b: {} });
|
|
266
|
+
return isNotFound(r);
|
|
267
|
+
})),
|
|
268
|
+
// ── Connector (D6/D7) ────────────────────────────────────────────────────────────────
|
|
269
|
+
done('polar.connector.sync.entrypoint', 'connector', 'syncPolarFromReal folds pulled customers/products into the twin', 'connector', 'core', () => withRoot(async (h) => {
|
|
270
|
+
const root = mkdtempSync(join(tmpdir(), 'polar-sync-'));
|
|
271
|
+
try {
|
|
272
|
+
const client = {
|
|
273
|
+
customers: { list: async () => ({ items: [{ id: 'cus_real_1', email: 'real@example.test', name: 'Real' }] }) },
|
|
274
|
+
products: { list: async () => ({ items: [{ id: 'prod_real_1', name: 'Real Plan', recurring_interval: 'month' }] }) },
|
|
275
|
+
subscriptions: { list: async () => ({ items: [] }) },
|
|
276
|
+
orders: { list: async () => ({ items: [] }) },
|
|
277
|
+
};
|
|
278
|
+
const first = await syncPolarFromReal(client, { root, occurredAt: '2026-01-01T00:00:00.000Z', budgetOptions: { root } });
|
|
279
|
+
const again = await syncPolarFromReal(client, { root, occurredAt: '2026-01-01T00:00:00.000Z', budgetOptions: { root } });
|
|
280
|
+
// pulled customer is now served by the twin's read path over the SAME root
|
|
281
|
+
const c = await handlePolarTwinRequest({ method: 'GET', path: '/v1/customers/cus_real_1', root });
|
|
282
|
+
return first.observed === 2 && first.deltasAppended === 2 && again.deltasAppended === 0 && c.status === 200 && field(c, 'email') === 'real@example.test' && Boolean(h);
|
|
283
|
+
}
|
|
284
|
+
finally {
|
|
285
|
+
rmSync(root, { recursive: true, force: true });
|
|
286
|
+
}
|
|
287
|
+
})),
|
|
288
|
+
done('polar.connector.push.entrypoint', 'connector', 'performPolarAction crosses a local customer.create to Polar through the executor (protocol 2)', 'connector', 'core', () => withRoot(async () => {
|
|
289
|
+
const root = mkdtempSync(join(tmpdir(), 'polar-push-'));
|
|
290
|
+
try {
|
|
291
|
+
const made = await handlePolarTwinRequest({ method: 'POST', path: '/v1/customers/', body: JSON.stringify({ email: 'push@example.test', name: 'P' }), root });
|
|
292
|
+
const wire = [];
|
|
293
|
+
const execute = async (r) => { wire.push(r); return { status: 201, headers: {}, body: JSON.stringify({ id: 'cus_pushed' }) }; };
|
|
294
|
+
const out = await performPolarAction(execute, { operation: 'customer.create', subject: { type: 'customer', id: `customer:${String(field(made, 'id'))}` }, fields: { email: 'push@example.test', name: 'P' } }, { resolve: (_t, l) => l, service: 'polar', root });
|
|
295
|
+
const sent = JSON.parse(typeof wire[0]?.body === 'string' ? wire[0].body : Buffer.from(wire[0]?.body ?? '{}').toString());
|
|
296
|
+
// one entry, one wire write, the vendor's id back; re-performing is the kernel's to refuse
|
|
297
|
+
return out.externalId === 'cus_pushed' && wire.length === 1 && wire[0].path === '/v1/customers' && sent.email === 'push@example.test';
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
rmSync(root, { recursive: true, force: true });
|
|
301
|
+
}
|
|
302
|
+
})),
|
|
303
|
+
done('polar.conformance.snapshot', 'conformance', 'Conformance snapshot covers implemented resource types and endpoints', 'api', 'common', () => {
|
|
304
|
+
const report = checkPolarConformance();
|
|
305
|
+
return report.ok && report.endpointsChecked >= 30 && report.resourceTypesChecked >= 10;
|
|
306
|
+
}),
|
|
307
|
+
// ── the remaining real Polar surface (the honest TODO denominator) ─────────────────────
|
|
308
|
+
...[
|
|
309
|
+
['products.prices_full', 'products', 'Full price modeling (fixed/recurring/metered/custom/tiered)'],
|
|
310
|
+
['products.benefits_attach', 'products', 'Attach/detach benefits to a product'],
|
|
311
|
+
['products.medias', 'products', 'Product media uploads and ordering'],
|
|
312
|
+
['products.custom_fields', 'products', 'Custom field attachment + validation'],
|
|
313
|
+
['prices.archive', 'products', 'Archive/replace a price'],
|
|
314
|
+
['benefits.grants', 'benefits', 'Grant/revoke customer benefits + benefit grants list'],
|
|
315
|
+
['benefits.types', 'benefits', 'Built-in benefit types (downloadables, license keys, GitHub, Discord)'],
|
|
316
|
+
['discounts.redemption', 'discounts', 'Discount redemption + max_redemptions enforcement'],
|
|
317
|
+
['meters.quantities', 'meters', 'Meter quantities aggregation + time buckets'],
|
|
318
|
+
['meters.update', 'meters', 'Update/archive a meter'],
|
|
319
|
+
['events.dedupe', 'events', 'Event idempotency / external_id duplicate detection'],
|
|
320
|
+
['events.external_customer', 'events', 'Ingest by external_customer_id resolution'],
|
|
321
|
+
['customer_sessions.scopes', 'customer-sessions', 'Customer portal session scopes + expiry enforcement'],
|
|
322
|
+
['customer_portal.api', 'customer-portal', 'Customer portal API (subscriptions/orders/benefit grants by session token)'],
|
|
323
|
+
['checkouts.update', 'checkouts', 'Update an open checkout (customer fields, discount, amount)'],
|
|
324
|
+
['checkouts.embed', 'checkouts', 'Embedded checkout embed_origin enforcement'],
|
|
325
|
+
['checkouts.expiry', 'checkouts', 'Checkout expiry transitions'],
|
|
326
|
+
['checkout_links.crud', 'checkouts', 'Reusable checkout links CRUD'],
|
|
327
|
+
['subscriptions.proration', 'subscriptions', 'Plan changes with proration'],
|
|
328
|
+
['subscriptions.trials', 'subscriptions', 'Trials and trial-end transitions'],
|
|
329
|
+
['subscriptions.revoke', 'subscriptions', 'Revoke (cancel immediately) vs cancel-at-period-end'],
|
|
330
|
+
['subscriptions.uncancel', 'subscriptions', 'Uncancel a subscription scheduled to cancel'],
|
|
331
|
+
['subscriptions.seats', 'subscriptions', 'Seat-based subscription quantity'],
|
|
332
|
+
['orders.invoice', 'orders', 'Generate/download an order invoice PDF'],
|
|
333
|
+
['orders.list_filters', 'orders', 'Order filters (product/status/date) + sorting'],
|
|
334
|
+
['refunds.crud', 'refunds', 'Refunds resource CRUD + partial refund accounting'],
|
|
335
|
+
['webhooks.delivery', 'webhooks', 'Webhook delivery, signing (standard-webhooks), and retries'],
|
|
336
|
+
['webhooks.deliveries_list', 'webhooks', 'Webhook delivery logs + redelivery'],
|
|
337
|
+
['webhooks.endpoint_update', 'webhooks', 'Update a webhook endpoint (url/events/secret)'],
|
|
338
|
+
['license_keys.crud', 'license-keys', 'License key create/activate/validate'],
|
|
339
|
+
['license_keys.limits', 'license-keys', 'Activation limits and revocation'],
|
|
340
|
+
['files.upload', 'files', 'File upload (S3 multipart) for downloadable benefits'],
|
|
341
|
+
['organizations.crud', 'organizations', 'Organization settings, branding, and account'],
|
|
342
|
+
['custom_fields.crud', 'custom-fields', 'Custom field definitions CRUD'],
|
|
343
|
+
['oauth.apps', 'oauth', 'OAuth2 app + token flows'],
|
|
344
|
+
['oauth.userinfo', 'oauth', 'OAuth userinfo + token introspection'],
|
|
345
|
+
['pagination.cursors', 'pagination', 'Page/limit pagination + sorting across list endpoints'],
|
|
346
|
+
['metadata.filter', 'metadata', 'Metadata filtering on list endpoints'],
|
|
347
|
+
['auth.token', 'auth', 'Organization access token auth (401 on bad token)'],
|
|
348
|
+
['rate_limits', 'limits', 'Rate-limit headers and retry semantics'],
|
|
349
|
+
['idempotency', 'idempotency', 'Idempotency-Key dedupe on mutating requests'],
|
|
350
|
+
['mirror.ui', 'ui', 'Operator mirror UI for customers/subscriptions/orders/events'],
|
|
351
|
+
['connector.subscriptions_orders', 'connector', 'Pull subscriptions/orders incrementally with cursors'],
|
|
352
|
+
['fixtures.seed', 'fixtures', 'Seed products, prices, and subscriptions from fixtures'],
|
|
353
|
+
].map(([id, area, title]) => todo(`polar.${id}`, area, title, id.startsWith('connector') ? 'connector' : id === 'mirror.ui' ? 'ui' : 'api', id.includes('webhooks') || id.includes('connector') || id.includes('auth') ? 'core' : 'common')),
|
|
354
|
+
// ── Pull-surface coverage audit gaps (TWIN-46 / G2) — filed as manifest todos, which is
|
|
355
|
+
// what the demand-ordered build list is drawn from. See pull-audit.json (repo root) for the
|
|
356
|
+
// per-pack pull-vs-read-surface evidence.
|
|
357
|
+
todo('polar.connector.pull_webhook_endpoints', 'connector', 'Connector: pull registered webhook endpoints from the real account', 'connector', 'core'),
|
|
358
|
+
todo('polar.connector.pull_benefits', 'connector', 'Connector: pull benefits from the real account', 'connector', 'common'),
|
|
359
|
+
todo('polar.connector.pull_discounts', 'connector', 'Connector: pull discounts from the real account', 'connector', 'common'),
|
|
360
|
+
// ── Missing-area sweep (TWIN-87 / F1) ────────────────────────────────────────────────────
|
|
361
|
+
// /v1/metrics and /v1/customer-meters had NO manifest entry of any status, and the
|
|
362
|
+
// `payments` area held only a real-processing phantom (list/read was unrepresented).
|
|
363
|
+
// Filed as honest todos (all genuinely buildable — reading back recorded payment metadata
|
|
364
|
+
// is not the same as real card processing, which is what the real root does). See
|
|
365
|
+
// POLAR_AREAS below + polar-capabilities.test.ts's area-census meta-test, gate-wired so a
|
|
366
|
+
// future whole-area omission fails here instead of waiting for another review pass.
|
|
367
|
+
todo('polar.metrics.query', 'metrics', 'Metrics: aggregated revenue/orders/subscribers time-series (GET /v1/metrics/)', 'api', 'niche'),
|
|
368
|
+
todo('polar.metrics.limits', 'metrics', 'Metrics: query granularity/date-range validation limits', 'api', 'niche'),
|
|
369
|
+
todo('polar.customer_meters.list', 'customer-meters', 'Customer meters: list per-customer meter balances (GET /v1/customer-meters/)', 'api', 'niche'),
|
|
370
|
+
todo('polar.customer_meters.get', 'customer-meters', 'Customer meters: retrieve a single customer-meter by id', 'api', 'niche'),
|
|
371
|
+
todo('polar.payments.list', 'payments', 'Payments: list recorded payment metadata (GET /v1/payments/)', 'api', 'niche'),
|
|
372
|
+
todo('polar.payments.get', 'payments', 'Payments: retrieve a single recorded payment by id', 'api', 'niche'),
|
|
373
|
+
];
|
|
374
|
+
// TWIN-87 committed area census — the vendor's top-level API product areas (docs nav /
|
|
375
|
+
// OpenAPI tags), authored top-down independent of what a manifest entry happens to already
|
|
376
|
+
// exist for. polar-capabilities.test.ts's area-census meta-test (assertAreaCensus) fails the
|
|
377
|
+
// gate if a declared area has zero manifest entries and no named exclusion, OR if a manifest
|
|
378
|
+
// entry's `area` drifts outside this list — so a whole missing area (metrics, customer-
|
|
379
|
+
// meters were the concrete F1 leak) can never again hide invisibly.
|
|
380
|
+
export const POLAR_AREAS = [
|
|
381
|
+
'auth', 'benefits', 'checkouts', 'conformance', 'connector', 'custom-fields', 'customer-meters',
|
|
382
|
+
'customer-portal', 'customer-sessions', 'customers', 'discounts', 'errors', 'events', 'files',
|
|
383
|
+
'fixtures', 'idempotency', 'license-keys', 'limits', 'metadata', 'meters', 'metrics', 'oauth',
|
|
384
|
+
'orders', 'organizations', 'pagination', 'payments', 'products', 'refunds', 'safety',
|
|
385
|
+
'subscriptions', 'tax', 'ui', 'webhooks',
|
|
386
|
+
];
|
|
387
|
+
export async function polarCapabilities() {
|
|
388
|
+
return checkCapabilities('polar', POLAR_CAPABILITIES);
|
|
389
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Polar conformance (dev-only; lazy-imported by the CLI, NEVER from index.ts/runtime — E2).
|
|
2
|
+
// Checks the twin's endpoint/resource inventory against the modeled Polar surface.
|
|
3
|
+
import { polarTwinSnapshot, POLAR_RESOURCE_TYPES } from "./polar-twin.js";
|
|
4
|
+
export function checkPolarConformance() {
|
|
5
|
+
const snapshot = polarTwinSnapshot();
|
|
6
|
+
const violations = [];
|
|
7
|
+
// Every declared resource type must have at least one implemented endpoint touching it.
|
|
8
|
+
for (const type of POLAR_RESOURCE_TYPES) {
|
|
9
|
+
const stem = type === 'customer_session' ? 'customer-sessions'
|
|
10
|
+
: type === 'webhook_endpoint' ? 'webhooks/endpoints'
|
|
11
|
+
: `${type}s`;
|
|
12
|
+
if (!snapshot.implementedEndpoints.some((e) => e.includes(`/v1/${stem}`))) {
|
|
13
|
+
violations.push(`resource type '${type}' has no implemented endpoint`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (!snapshot.implementedEndpoints.some((e) => e.startsWith('POST /v1/customers/'))) {
|
|
17
|
+
violations.push('missing customer create endpoint');
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
ok: violations.length === 0,
|
|
21
|
+
endpointsChecked: snapshot.implementedEndpoints.length,
|
|
22
|
+
resourceTypesChecked: snapshot.resourceTypes.length,
|
|
23
|
+
violations,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { PerformContext, PushOutcome, RemoteExecute, SyncResource, TwinAction } from '@volter/world-core';
|
|
2
|
+
import { type PolarBudgetedOptions } from './polar-budget.js';
|
|
3
|
+
export type { PolarBudgetedOptions };
|
|
4
|
+
export type PolarCustomer = {
|
|
5
|
+
id: string;
|
|
6
|
+
email?: string | null;
|
|
7
|
+
name?: string | null;
|
|
8
|
+
external_id?: string | null;
|
|
9
|
+
created_at?: string;
|
|
10
|
+
metadata?: Record<string, unknown>;
|
|
11
|
+
};
|
|
12
|
+
export type PolarProduct = {
|
|
13
|
+
id: string;
|
|
14
|
+
name?: string | null;
|
|
15
|
+
description?: string | null;
|
|
16
|
+
recurring_interval?: string | null;
|
|
17
|
+
is_archived?: boolean;
|
|
18
|
+
created_at?: string;
|
|
19
|
+
prices?: unknown[];
|
|
20
|
+
trial_interval?: string | null;
|
|
21
|
+
trial_interval_count?: number | null;
|
|
22
|
+
};
|
|
23
|
+
export type PolarSubscription = {
|
|
24
|
+
id: string;
|
|
25
|
+
status?: string;
|
|
26
|
+
amount?: number;
|
|
27
|
+
currency?: string;
|
|
28
|
+
recurring_interval?: string;
|
|
29
|
+
customer_id?: string | null;
|
|
30
|
+
product_id?: string | null;
|
|
31
|
+
created_at?: string;
|
|
32
|
+
current_period_end?: string | null;
|
|
33
|
+
cancel_at_period_end?: boolean;
|
|
34
|
+
};
|
|
35
|
+
export type PolarOrder = {
|
|
36
|
+
id: string;
|
|
37
|
+
status?: string;
|
|
38
|
+
amount?: number;
|
|
39
|
+
total_amount?: number;
|
|
40
|
+
currency?: string;
|
|
41
|
+
customer_id?: string | null;
|
|
42
|
+
product_id?: string | null;
|
|
43
|
+
subscription_id?: string | null;
|
|
44
|
+
created_at?: string;
|
|
45
|
+
};
|
|
46
|
+
export interface PolarLikeClient {
|
|
47
|
+
customers?: {
|
|
48
|
+
list: (params?: Record<string, unknown>) => Promise<{
|
|
49
|
+
items: PolarCustomer[];
|
|
50
|
+
}>;
|
|
51
|
+
create?: (params: Record<string, unknown>) => Promise<{
|
|
52
|
+
id: string;
|
|
53
|
+
}>;
|
|
54
|
+
};
|
|
55
|
+
products?: {
|
|
56
|
+
list: (params?: Record<string, unknown>) => Promise<{
|
|
57
|
+
items: PolarProduct[];
|
|
58
|
+
}>;
|
|
59
|
+
create?: (params: Record<string, unknown>) => Promise<{
|
|
60
|
+
id: string;
|
|
61
|
+
}>;
|
|
62
|
+
};
|
|
63
|
+
subscriptions?: {
|
|
64
|
+
list: (params?: Record<string, unknown>) => Promise<{
|
|
65
|
+
items: PolarSubscription[];
|
|
66
|
+
}>;
|
|
67
|
+
};
|
|
68
|
+
orders?: {
|
|
69
|
+
list: (params?: Record<string, unknown>) => Promise<{
|
|
70
|
+
items: PolarOrder[];
|
|
71
|
+
}>;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export declare function mapCustomer(c: PolarCustomer): SyncResource;
|
|
75
|
+
export declare function mapProduct(p: PolarProduct): SyncResource;
|
|
76
|
+
export declare function mapSubscription(s: PolarSubscription): SyncResource;
|
|
77
|
+
export declare function mapOrder(o: PolarOrder): SyncResource;
|
|
78
|
+
export declare function pullPolarCustomers(rawClient: PolarLikeClient, opts?: PolarBudgetedOptions): Promise<SyncResource[]>;
|
|
79
|
+
export declare function pullPolarProducts(rawClient: PolarLikeClient, opts?: PolarBudgetedOptions): Promise<SyncResource[]>;
|
|
80
|
+
export declare function pullPolarSubscriptions(rawClient: PolarLikeClient, opts?: PolarBudgetedOptions): Promise<SyncResource[]>;
|
|
81
|
+
export declare function pullPolarOrders(rawClient: PolarLikeClient, opts?: PolarBudgetedOptions): Promise<SyncResource[]>;
|
|
82
|
+
/**
|
|
83
|
+
* D7 entry point: pull from real Polar (customers + products + subscriptions + orders) and fold
|
|
84
|
+
* EVERY domain into the twin via ONE syncPull (shadow-diff dedup). Returns { observed,
|
|
85
|
+
* deltasAppended } — callers need not know vendor-specific pull mechanics.
|
|
86
|
+
*/
|
|
87
|
+
export declare function syncPolarFromReal(rawClient: PolarLikeClient, opts?: {
|
|
88
|
+
root?: string;
|
|
89
|
+
occurredAt?: string;
|
|
90
|
+
} & PolarBudgetedOptions): Promise<{
|
|
91
|
+
observed: number;
|
|
92
|
+
deltasAppended: number;
|
|
93
|
+
}>;
|
|
94
|
+
/** A PolarLikeClient over a RemoteExecute: the calls the SDK makes, as wire requests the executor carries. */
|
|
95
|
+
export declare function polarClientOver(execute: RemoteExecute): PolarLikeClient;
|
|
96
|
+
/** The refresh adapter: pull Polar's customers, products, subscriptions and orders through the executor and fold them into the root. */
|
|
97
|
+
export declare function syncPolarFromRemote(execute: RemoteExecute, opts?: {
|
|
98
|
+
root?: string;
|
|
99
|
+
origin?: string;
|
|
100
|
+
occurredAt?: string;
|
|
101
|
+
} & PolarBudgetedOptions): Promise<{
|
|
102
|
+
observed: number;
|
|
103
|
+
deltasAppended: number;
|
|
104
|
+
}>;
|
|
105
|
+
/** The perform adapter: one entry crosses to Polar through the executor; a customer or product it names by a local id resolves first. */
|
|
106
|
+
export declare function performPolarAction(execute: RemoteExecute, action: TwinAction, ctx: PerformContext): Promise<PushOutcome>;
|
|
107
|
+
export declare function pushPolarAction(rawClient: PolarLikeClient, action: Pick<TwinAction, 'operation' | 'subject' | 'fields'>, opts?: PolarBudgetedOptions): Promise<{
|
|
108
|
+
externalId: string;
|
|
109
|
+
}>;
|
|
110
|
+
/**
|
|
111
|
+
* Push the twin's PENDING local actions to real Polar and CONFIRM each. A confirmed action is
|
|
112
|
+
* no longer pending, so a re-push enacts NOTHING. Unpushable ops are skipped.
|
|
113
|
+
*/
|