@volter/twin-polar 0.1.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.
@@ -0,0 +1,828 @@
1
+ // Polar API twin REQUEST HANDLER — the canonical api.polar.sh surface for the twin, backed by
2
+ // the event/action-log kernel (@volter/twin). Contract:
3
+ // handlePolarTwinRequest({ method, path, body, root, readOnly }) -> { status, body }
4
+ // Response SHAPES mirror real Polar (UUID4 ids, `{ items, pagination }` list envelopes, the
5
+ // FastAPI-style error envelopes) so the real `@polar-sh/sdk` works unmodified. HTTP wrapper:
6
+ // polar-server.ts → createPolarTwinServer.
7
+ //
8
+ // State lives ENTIRELY in the kernel action log: writes go through `applyTwinWrite`, reads are
9
+ // the projection (`projectResources`). There is NO Map/array side-store — the projection over
10
+ // the append-only log is the single source of truth (D1). No real Polar is ever contacted.
11
+ //
12
+ // Honesty (D2): an unmodeled route/op returns Polar's REAL error envelope (ResourceNotFound /
13
+ // HTTPValidationError), never a fabricated `{ ok: true }`. readOnly rejects writes with 405.
14
+ //
15
+ // Kernel SUBJECT ids are type-prefixed (`customer:<uuid>`) — a pack convention that keeps
16
+ // subject ids self-describing (the kernel resolves writes by (type, id), so the prefix is not
17
+ // required for correctness) — while the PUBLIC `id` emitted to clients is the bare UUID4 that
18
+ // Polar returns. `kid()` builds the subject id; rows() strips the prefix back off for the view.
19
+ import { createHash } from 'node:crypto';
20
+ import { applyTwinWrite, projectResources, worldNow } from '@volter/twin';
21
+
22
+ const SERVICE = 'polar';
23
+ const ORG_ID = '00000000-0000-4000-8000-0000000000a1'; // the twin's single organization id
24
+
25
+ export type PolarRequest = {
26
+ method: string;
27
+ path: string;
28
+ body?: string;
29
+ occurredAt?: string;
30
+ root?: string;
31
+ readOnly?: boolean;
32
+ };
33
+ export type PolarResponse = { status: number; body: unknown; headers?: Record<string, string> };
34
+
35
+ // Resource types the twin serves (kernel subject `type`s). The PUBLIC ids are bare UUIDs; the
36
+ // kernel subject id is `${type}:${uuid}` (collision-safe).
37
+ export const POLAR_RESOURCE_TYPES = [
38
+ 'customer', 'product', 'subscription', 'order', 'checkout', 'benefit',
39
+ 'discount', 'customer_session', 'event', 'meter', 'webhook_endpoint',
40
+ ] as const;
41
+ export type PolarResourceType = typeof POLAR_RESOURCE_TYPES[number];
42
+
43
+ // ── error envelopes (Polar / FastAPI shapes, D2) ─────────────────────────────────────
44
+ // Polar returns `{ type: 'ResourceNotFound', detail: '... not found.' }` for 404 and a
45
+ // FastAPI `HTTPValidationError` (`{ detail: [{ type, loc, msg }] }`) for 422.
46
+ function notFound(detail: string): PolarResponse {
47
+ return { status: 404, body: { type: 'ResourceNotFound', detail } };
48
+ }
49
+ function validationError(loc: (string | number)[], msg: string, type = 'value_error'): PolarResponse {
50
+ return { status: 422, body: { detail: [{ type, loc: ['body', ...loc], msg, input: null }] } };
51
+ }
52
+ function notPermitted(detail: string): PolarResponse {
53
+ return { status: 403, body: { type: 'NotPermitted', detail } };
54
+ }
55
+
56
+ // ── body / time / id helpers ─────────────────────────────────────────────────────────
57
+ function parseBody(body?: string): Record<string, unknown> {
58
+ if (!body) return {};
59
+ try {
60
+ const v = JSON.parse(body);
61
+ return v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : {};
62
+ } catch {
63
+ return {};
64
+ }
65
+ }
66
+ /** The instant this request happened at: the caller's `occurredAt` (the kernel fetch adapter
67
+ * stamps it from the world clock) or, for a direct in-process call that supplies none, the WORLD
68
+ * clock — never `new Date()`. Every stored/served timestamp and every minted id derives from it. */
69
+ function nowIso(occurredAt?: string): string {
70
+ return occurredAt ?? worldNow();
71
+ }
72
+ function laterIso(occurredAt?: string): string {
73
+ return new Date(Date.parse(nowIso(occurredAt)) + 30 * 24 * 60 * 60 * 1000).toISOString();
74
+ }
75
+
76
+ // ── ids: DERIVED FROM THE WRITE, never drawn ─────────────────────────────────────────
77
+ // Polar's public ids are bare UUID4s, so the SHAPE is a uuid4 (version nibble `4`, variant
78
+ // nibble `8`-`b`). The BITS are a SHA-256 of the write — the subject type, the world instant the
79
+ // request occurred at, and the ordinal the subject takes among its type — never `Math.random`.
80
+ //
81
+ // That is invariant R9 at the resource level: a served id must be a function of (request, stored
82
+ // state) and of nothing else, so two identical worlds replaying the same writes serve the same
83
+ // bytes. The earlier version drew every id from `Math.random` and called collisions the only
84
+ // risk; the real cost was that a replay could never be compared at all, and one `Math.random`
85
+ // sat on a READ path (`/customers/:id/state`'s `active_meters[].id`), so the same GET differed
86
+ // between two serves of unchanged state.
87
+ //
88
+ // The ordinal is the count of live rows of that type BEFORE the insert, which is what keeps two
89
+ // subjects created in the SAME millisecond apart; every call site mints and then AWAITS its
90
+ // write, so the next mint of that type sees a count one larger.
91
+ function digest(kind: string, seed: string): string {
92
+ return createHash('sha256').update(`polar-twin-id:${kind}:${seed}`).digest('hex');
93
+ }
94
+ function uuidFrom(kind: string, seed: string): string {
95
+ const h = digest(kind, seed);
96
+ const variant = ((parseInt(h[16]!, 16) & 0x3) | 0x8).toString(16);
97
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-${variant}${h.slice(17, 20)}-${h.slice(20, 32)}`;
98
+ }
99
+ /** The public id for a NEW subject of `type`, derived from this request and the ordinal it takes. */
100
+ function mintId(type: string, req: PolarRequest, extra = ''): string {
101
+ return uuidFrom(type, `${nowIso(req.occurredAt)}:${rows(type, req.root).length}:${extra}`);
102
+ }
103
+ /** A 32-hex secret (session token, checkout client secret), derived the same way. */
104
+ function mintSecret(kind: string, req: PolarRequest, extra = ''): string {
105
+ return digest(kind, `${nowIso(req.occurredAt)}:${extra}`).slice(0, 32);
106
+ }
107
+ // Kernel subject id (type-prefixed → collision-safe across types).
108
+ function kid(type: string, id: string): string {
109
+ return `${type}:${id}`;
110
+ }
111
+
112
+ // ── projection helpers ──────────────────────────────────────────────────────────────
113
+ // Live (non-soft-deleted) rows of a type, with the kernel subject prefix stripped back to the
114
+ // bare public id. `_deleted` is the twin-internal soft-delete overlay (the log is append-only).
115
+ function rows(type: string, root?: string): Array<Record<string, unknown>> {
116
+ const prefix = `${type}:`;
117
+ return projectResources(SERVICE, root)
118
+ .filter((r) => r.type === type && r.id.startsWith(prefix) && (r as Record<string, unknown>)._deleted !== true)
119
+ .map((r) => ({ ...r, id: r.id.slice(prefix.length) }));
120
+ }
121
+ function getRow(type: string, id: string, root?: string): Record<string, unknown> | undefined {
122
+ return rows(type, root).find((r) => r.id === id);
123
+ }
124
+ // The client view: drop kernel meta (`type`, `updatedAt`) + twin-internal `_`-prefixed fields.
125
+ function view(r: Record<string, unknown>): Record<string, unknown> {
126
+ const { type: _t, updatedAt: _u, ...rest } = r;
127
+ const out: Record<string, unknown> = {};
128
+ for (const [k, v] of Object.entries(rest)) if (!k.startsWith('_')) out[k] = v;
129
+ return out;
130
+ }
131
+
132
+ async function write(
133
+ type: string,
134
+ id: string,
135
+ fields: Record<string, unknown>,
136
+ op: string,
137
+ req: PolarRequest,
138
+ ): Promise<Record<string, unknown>> {
139
+ const { resource } = await applyTwinWrite(
140
+ SERVICE,
141
+ { operation: op, subjectType: type, subjectId: kid(type, id), fields, ...(req.occurredAt ? { occurredAt: req.occurredAt } : {}), actor: { kind: 'agent' } },
142
+ req.root,
143
+ );
144
+ return view({ ...resource, id });
145
+ }
146
+
147
+ // Polar's list envelope: `{ items: [...], pagination: { total_count, max_page } }`.
148
+ function listEnvelope(items: unknown[]): { items: unknown[]; pagination: { total_count: number; max_page: number } } {
149
+ return { items, pagination: { total_count: items.length, max_page: items.length > 0 ? 1 : 0 } };
150
+ }
151
+
152
+ // ── object shapes (mirror real Polar response bodies) ────────────────────────────────
153
+ function customerView(r: Record<string, unknown>): Record<string, unknown> {
154
+ const v = view(r);
155
+ return {
156
+ id: v.id,
157
+ created_at: v.created_at,
158
+ modified_at: v.modified_at ?? null,
159
+ metadata: v.metadata ?? {},
160
+ external_id: v.external_id ?? null,
161
+ email: v.email ?? null,
162
+ email_verified: v.email_verified ?? false,
163
+ type: v.customer_type ?? 'individual',
164
+ name: v.name ?? null,
165
+ billing_name: v.billing_name ?? null,
166
+ billing_address: v.billing_address ?? null,
167
+ tax_id: v.tax_id ?? null,
168
+ locale: v.locale ?? 'en',
169
+ organization_id: ORG_ID,
170
+ default_payment_method_id: null,
171
+ deleted_at: null,
172
+ avatar_url: `https://www.gravatar.com/avatar?d=blank`,
173
+ };
174
+ }
175
+
176
+ function productView(r: Record<string, unknown>): Record<string, unknown> {
177
+ const v = view(r);
178
+ return {
179
+ id: v.id,
180
+ created_at: v.created_at,
181
+ modified_at: v.modified_at ?? null,
182
+ name: v.name ?? null,
183
+ description: v.description ?? null,
184
+ recurring_interval: v.recurring_interval ?? null,
185
+ is_recurring: v.recurring_interval != null,
186
+ is_archived: v.is_archived ?? false,
187
+ organization_id: ORG_ID,
188
+ metadata: v.metadata ?? {},
189
+ prices: Array.isArray(v.prices) ? v.prices : [],
190
+ benefits: Array.isArray(v.benefits) ? v.benefits : [],
191
+ medias: [],
192
+ attached_custom_fields: [],
193
+ };
194
+ }
195
+
196
+ function subscriptionView(r: Record<string, unknown>, root?: string): Record<string, unknown> {
197
+ const v = view(r);
198
+ const cust = v.customer_id ? getRow('customer', String(v.customer_id), root) : undefined;
199
+ return {
200
+ id: v.id,
201
+ created_at: v.created_at,
202
+ modified_at: v.modified_at ?? null,
203
+ amount: v.amount ?? 0,
204
+ currency: v.currency ?? 'usd',
205
+ recurring_interval: v.recurring_interval ?? 'month',
206
+ status: v.status ?? 'active',
207
+ current_period_start: v.current_period_start ?? v.created_at,
208
+ current_period_end: v.current_period_end ?? null,
209
+ cancel_at_period_end: v.cancel_at_period_end ?? false,
210
+ canceled_at: v.canceled_at ?? null,
211
+ started_at: v.started_at ?? v.created_at,
212
+ ends_at: v.ends_at ?? null,
213
+ ended_at: v.ended_at ?? null,
214
+ customer_id: v.customer_id ?? null,
215
+ product_id: v.product_id ?? null,
216
+ discount_id: v.discount_id ?? null,
217
+ checkout_id: v.checkout_id ?? null,
218
+ metadata: v.metadata ?? {},
219
+ customer: cust ? customerView(cust) : null,
220
+ prices: [],
221
+ };
222
+ }
223
+
224
+ function orderView(r: Record<string, unknown>): Record<string, unknown> {
225
+ const v = view(r);
226
+ return {
227
+ id: v.id,
228
+ created_at: v.created_at,
229
+ modified_at: v.modified_at ?? null,
230
+ status: v.status ?? 'paid',
231
+ paid: v.status === 'refunded' ? false : (v.paid ?? true),
232
+ subtotal_amount: v.subtotal_amount ?? v.amount ?? 0,
233
+ discount_amount: v.discount_amount ?? 0,
234
+ net_amount: v.net_amount ?? v.amount ?? 0,
235
+ tax_amount: v.tax_amount ?? 0,
236
+ total_amount: v.total_amount ?? v.amount ?? 0,
237
+ refunded_amount: v.refunded_amount ?? 0,
238
+ currency: v.currency ?? 'usd',
239
+ billing_reason: v.billing_reason ?? 'purchase',
240
+ customer_id: v.customer_id ?? null,
241
+ product_id: v.product_id ?? null,
242
+ subscription_id: v.subscription_id ?? null,
243
+ checkout_id: v.checkout_id ?? null,
244
+ metadata: v.metadata ?? {},
245
+ };
246
+ }
247
+
248
+ function checkoutView(r: Record<string, unknown>): Record<string, unknown> {
249
+ const v = view(r);
250
+ return {
251
+ id: v.id,
252
+ created_at: v.created_at,
253
+ modified_at: v.modified_at ?? null,
254
+ payment_processor: 'stripe',
255
+ status: v.status ?? 'open',
256
+ client_secret: v.client_secret,
257
+ url: v.url,
258
+ expires_at: v.expires_at,
259
+ success_url: v.success_url ?? null,
260
+ embed_origin: v.embed_origin ?? null,
261
+ amount: v.amount ?? 0,
262
+ discount_amount: 0,
263
+ net_amount: v.amount ?? 0,
264
+ tax_amount: 0,
265
+ total_amount: v.amount ?? 0,
266
+ currency: v.currency ?? 'usd',
267
+ product_id: v.product_id ?? null,
268
+ products: Array.isArray(v.products) ? v.products : [],
269
+ discount_id: v.discount_id ?? null,
270
+ customer_id: v.customer_id ?? null,
271
+ customer_email: v.customer_email ?? null,
272
+ customer_name: v.customer_name ?? null,
273
+ subscription_id: v.subscription_id ?? null,
274
+ organization_id: ORG_ID,
275
+ metadata: v.metadata ?? {},
276
+ customer_metadata: {},
277
+ };
278
+ }
279
+
280
+ function benefitView(r: Record<string, unknown>): Record<string, unknown> {
281
+ const v = view(r);
282
+ return {
283
+ id: v.id,
284
+ created_at: v.created_at,
285
+ modified_at: v.modified_at ?? null,
286
+ type: v.benefit_type ?? 'custom',
287
+ description: v.description ?? null,
288
+ selectable: v.selectable ?? true,
289
+ deletable: v.deletable ?? true,
290
+ organization_id: ORG_ID,
291
+ properties: v.properties ?? {},
292
+ metadata: v.metadata ?? {},
293
+ };
294
+ }
295
+
296
+ function discountView(r: Record<string, unknown>): Record<string, unknown> {
297
+ const v = view(r);
298
+ return {
299
+ id: v.id,
300
+ created_at: v.created_at,
301
+ modified_at: v.modified_at ?? null,
302
+ name: v.name ?? null,
303
+ code: v.code ?? null,
304
+ type: v.discount_type ?? 'percentage',
305
+ basis_points: v.basis_points ?? null,
306
+ amount: v.amount ?? null,
307
+ currency: v.currency ?? null,
308
+ duration: v.duration ?? 'once',
309
+ max_redemptions: v.max_redemptions ?? null,
310
+ redemptions_count: v.redemptions_count ?? 0,
311
+ organization_id: ORG_ID,
312
+ metadata: v.metadata ?? {},
313
+ };
314
+ }
315
+
316
+ function meterView(r: Record<string, unknown>): Record<string, unknown> {
317
+ const v = view(r);
318
+ return {
319
+ id: v.id,
320
+ created_at: v.created_at,
321
+ modified_at: v.modified_at ?? null,
322
+ name: v.name ?? null,
323
+ aggregation: v.aggregation ?? { func: 'count' },
324
+ filter: v.filter ?? { conjunction: 'and', clauses: [] },
325
+ organization_id: ORG_ID,
326
+ metadata: v.metadata ?? {},
327
+ };
328
+ }
329
+
330
+ function eventView(r: Record<string, unknown>): Record<string, unknown> {
331
+ const v = view(r);
332
+ return {
333
+ id: v.id,
334
+ timestamp: v.timestamp ?? v.created_at,
335
+ organization_id: ORG_ID,
336
+ customer_id: v.customer_id ?? null,
337
+ external_customer_id: v.external_customer_id ?? null,
338
+ name: v.name ?? null,
339
+ source: v.source ?? 'user',
340
+ metadata: v.metadata ?? {},
341
+ };
342
+ }
343
+
344
+ function webhookView(r: Record<string, unknown>): Record<string, unknown> {
345
+ const v = view(r);
346
+ return {
347
+ id: v.id,
348
+ created_at: v.created_at,
349
+ modified_at: v.modified_at ?? null,
350
+ url: v.url ?? null,
351
+ format: v.format ?? 'raw',
352
+ events: Array.isArray(v.events) ? v.events : [],
353
+ organization_id: ORG_ID,
354
+ };
355
+ }
356
+
357
+ // ── customer state (meter balances) ──────────────────────────────────────────────────
358
+ function customerState(customerId: string, root?: string): Record<string, unknown> {
359
+ const cust = getRow('customer', customerId, root);
360
+ const subs = rows('subscription', root)
361
+ .filter((s) => s.customer_id === customerId && s.status === 'active')
362
+ .map((s) => subscriptionView(s, root));
363
+ const meters = rows('event', root)
364
+ .filter((e) => e.customer_id === customerId)
365
+ .reduce<{ consumed: number; credited: number }>((acc, e) => {
366
+ const cost = Number((e.metadata as Record<string, unknown>)?.cost_usd ?? 0);
367
+ if (Number.isFinite(cost)) { if (cost < 0) acc.credited += Math.abs(cost); else acc.consumed += cost; }
368
+ return acc;
369
+ }, { consumed: 0, credited: 0 });
370
+ return {
371
+ ...(cust ? customerView(cust) : { id: customerId }),
372
+ active_subscriptions: subs,
373
+ granted_benefits: [],
374
+ active_meters: [{
375
+ id: uuidFrom('customer_meter', customerId),
376
+ meter_id: '00000000-0000-4000-8000-0000000000b1',
377
+ consumed_units: meters.consumed,
378
+ credited_units: meters.credited,
379
+ balance: meters.credited - meters.consumed,
380
+ }],
381
+ };
382
+ }
383
+
384
+ // ── route handler ────────────────────────────────────────────────────────────────────
385
+ export async function handlePolarTwinRequest(req: PolarRequest): Promise<PolarResponse> {
386
+ const method = req.method.toUpperCase();
387
+ const [rawPath, rawQuery] = req.path.split('?');
388
+ const path = (rawPath ?? '/').replace(/\/+$/, '') || '/';
389
+ const query = Object.fromEntries(new URLSearchParams(rawQuery ?? ''));
390
+ const body = parseBody(req.body);
391
+ // segments after the /v1 prefix, e.g. /v1/customers/abc → ["customers","abc"]
392
+ const all = path.replace(/^\/+/, '').split('/').filter(Boolean);
393
+ const seg = all[0] === 'v1' ? all.slice(1) : all;
394
+ const idAt = (i: number) => decodeURIComponent(seg[i] ?? '');
395
+
396
+ if (req.readOnly && method !== 'GET') {
397
+ return { status: 405, body: { type: 'NotPermitted', detail: 'twin is read-only; omit readOnly to accept writes' } };
398
+ }
399
+
400
+ if (path === '/' || seg.length === 0) {
401
+ return { status: 200, body: { service: 'polar', object: 'twin', organization_id: ORG_ID } };
402
+ }
403
+
404
+ // ── CUSTOMERS ──
405
+ if (seg[0] === 'customers') {
406
+ if (seg.length === 1 && method === 'POST') {
407
+ if (!body.email && (body.type ?? 'individual') === 'individual') {
408
+ return validationError(['email'], 'Field required', 'missing');
409
+ }
410
+ const email = body.email ? String(body.email) : null;
411
+ if (email && rows('customer', req.root).some((c) => c.email === email)) {
412
+ return { status: 422, body: { type: 'PolarRequestValidationError', detail: [{ type: 'value_error', loc: ['body', 'email'], msg: 'A customer with this email address already exists.' }] } };
413
+ }
414
+ const id = mintId('customer', req);
415
+ const c = await write('customer', id, {
416
+ created_at: nowIso(req.occurredAt), modified_at: null,
417
+ email, name: body.name ?? null, external_id: body.external_id ?? null,
418
+ customer_type: body.type ?? 'individual', email_verified: false,
419
+ billing_name: body.billing_name ?? null, billing_address: body.billing_address ?? null,
420
+ tax_id: body.tax_id ?? null, locale: body.locale ?? 'en',
421
+ metadata: body.metadata ?? {},
422
+ }, 'customer.create', req);
423
+ return { status: 201, body: customerView({ ...c, type: 'customer' }) };
424
+ }
425
+ if (seg.length === 1 && method === 'GET') {
426
+ const email = query.email;
427
+ const extId = query.external_id;
428
+ const items = rows('customer', req.root)
429
+ .filter((c) => (!email || c.email === email) && (!extId || c.external_id === extId))
430
+ .map((c) => customerView({ ...c, type: 'customer' }));
431
+ return { status: 200, body: listEnvelope(items) };
432
+ }
433
+ // GET /customers/:id/state
434
+ if (seg.length === 3 && seg[2] === 'state' && method === 'GET') {
435
+ if (!getRow('customer', idAt(1), req.root)) return notFound('Customer not found.');
436
+ return { status: 200, body: customerState(idAt(1), req.root) };
437
+ }
438
+ if (seg.length === 2 && method === 'GET') {
439
+ const c = getRow('customer', idAt(1), req.root);
440
+ return c ? { status: 200, body: customerView({ ...c, type: 'customer' }) } : notFound('Customer not found.');
441
+ }
442
+ if (seg.length === 2 && method === 'PATCH') {
443
+ const c = getRow('customer', idAt(1), req.root);
444
+ if (!c) return notFound('Customer not found.');
445
+ const patch: Record<string, unknown> = { modified_at: nowIso(req.occurredAt) };
446
+ for (const k of ['email', 'name', 'external_id', 'billing_name', 'billing_address', 'tax_id', 'locale', 'metadata']) if (k in body) patch[k] = body[k];
447
+ const updated = await write('customer', idAt(1), patch, 'customer.update', req);
448
+ return { status: 200, body: customerView({ ...updated, type: 'customer' }) };
449
+ }
450
+ if (seg.length === 2 && method === 'DELETE') {
451
+ const c = getRow('customer', idAt(1), req.root);
452
+ if (!c) return notFound('Customer not found.');
453
+ await write('customer', idAt(1), { _deleted: true }, 'customer.delete', req);
454
+ return { status: 204, body: null };
455
+ }
456
+ }
457
+
458
+ // ── PRODUCTS ──
459
+ if (seg[0] === 'products') {
460
+ if (seg.length === 1 && method === 'POST') {
461
+ if (!body.name) return validationError(['name'], 'Field required', 'missing');
462
+ const prices = Array.isArray(body.prices)
463
+ ? (body.prices as Array<Record<string, unknown>>).map((p, i) => ({ id: uuidFrom('price', `${nowIso(req.occurredAt)}:${rows('product', req.root).length}:${i}`), ...p }))
464
+ : [];
465
+ const id = mintId('product', req);
466
+ const p = await write('product', id, {
467
+ created_at: nowIso(req.occurredAt), modified_at: null,
468
+ name: body.name, description: body.description ?? null,
469
+ recurring_interval: body.recurring_interval ?? null,
470
+ is_archived: false, prices, benefits: [], metadata: body.metadata ?? {},
471
+ }, 'product.create', req);
472
+ return { status: 201, body: productView({ ...p, type: 'product' }) };
473
+ }
474
+ if (seg.length === 1 && method === 'GET') {
475
+ let items = rows('product', req.root);
476
+ if (query.is_archived !== undefined) items = items.filter((p) => Boolean(p.is_archived) === (query.is_archived === 'true'));
477
+ return { status: 200, body: listEnvelope(items.map((p) => productView({ ...p, type: 'product' }))) };
478
+ }
479
+ if (seg.length === 2 && method === 'GET') {
480
+ const p = getRow('product', idAt(1), req.root);
481
+ return p ? { status: 200, body: productView({ ...p, type: 'product' }) } : notFound('Product not found.');
482
+ }
483
+ if (seg.length === 2 && method === 'PATCH') {
484
+ const p = getRow('product', idAt(1), req.root);
485
+ if (!p) return notFound('Product not found.');
486
+ const patch: Record<string, unknown> = { modified_at: nowIso(req.occurredAt) };
487
+ for (const k of ['name', 'description', 'recurring_interval', 'is_archived', 'metadata']) if (k in body) patch[k] = body[k];
488
+ const updated = await write('product', idAt(1), patch, 'product.update', req);
489
+ return { status: 200, body: productView({ ...updated, type: 'product' }) };
490
+ }
491
+ }
492
+
493
+ // ── BENEFITS ──
494
+ if (seg[0] === 'benefits') {
495
+ if (seg.length === 1 && method === 'POST') {
496
+ if (!body.type) return validationError(['type'], 'Field required', 'missing');
497
+ const id = mintId('benefit', req);
498
+ const b = await write('benefit', id, {
499
+ created_at: nowIso(req.occurredAt), modified_at: null,
500
+ benefit_type: body.type, description: body.description ?? null,
501
+ properties: body.properties ?? {}, metadata: body.metadata ?? {},
502
+ }, 'benefit.create', req);
503
+ return { status: 201, body: benefitView({ ...b, type: 'benefit' }) };
504
+ }
505
+ if (seg.length === 1 && method === 'GET') {
506
+ return { status: 200, body: listEnvelope(rows('benefit', req.root).map((b) => benefitView({ ...b, type: 'benefit' }))) };
507
+ }
508
+ if (seg.length === 2 && method === 'GET') {
509
+ const b = getRow('benefit', idAt(1), req.root);
510
+ return b ? { status: 200, body: benefitView({ ...b, type: 'benefit' }) } : notFound('Benefit not found.');
511
+ }
512
+ if (seg.length === 2 && method === 'DELETE') {
513
+ const b = getRow('benefit', idAt(1), req.root);
514
+ if (!b) return notFound('Benefit not found.');
515
+ await write('benefit', idAt(1), { _deleted: true }, 'benefit.delete', req);
516
+ return { status: 204, body: null };
517
+ }
518
+ }
519
+
520
+ // ── DISCOUNTS ──
521
+ if (seg[0] === 'discounts') {
522
+ if (seg.length === 1 && method === 'POST') {
523
+ if (!body.name) return validationError(['name'], 'Field required', 'missing');
524
+ if (!body.type) return validationError(['type'], 'Field required', 'missing');
525
+ const id = mintId('discount', req);
526
+ const d = await write('discount', id, {
527
+ created_at: nowIso(req.occurredAt), modified_at: null,
528
+ name: body.name, code: body.code ?? null, discount_type: body.type,
529
+ basis_points: body.basis_points ?? null, amount: body.amount ?? null,
530
+ currency: body.currency ?? null, duration: body.duration ?? 'once',
531
+ max_redemptions: body.max_redemptions ?? null, redemptions_count: 0,
532
+ metadata: body.metadata ?? {},
533
+ }, 'discount.create', req);
534
+ return { status: 201, body: discountView({ ...d, type: 'discount' }) };
535
+ }
536
+ if (seg.length === 1 && method === 'GET') {
537
+ return { status: 200, body: listEnvelope(rows('discount', req.root).map((d) => discountView({ ...d, type: 'discount' }))) };
538
+ }
539
+ if (seg.length === 2 && method === 'GET') {
540
+ const d = getRow('discount', idAt(1), req.root);
541
+ return d ? { status: 200, body: discountView({ ...d, type: 'discount' }) } : notFound('Discount not found.');
542
+ }
543
+ if (seg.length === 2 && method === 'PATCH') {
544
+ const d = getRow('discount', idAt(1), req.root);
545
+ if (!d) return notFound('Discount not found.');
546
+ const patch: Record<string, unknown> = { modified_at: nowIso(req.occurredAt) };
547
+ for (const k of ['name', 'code', 'basis_points', 'amount', 'duration', 'max_redemptions', 'metadata']) if (k in body) patch[k] = body[k];
548
+ const updated = await write('discount', idAt(1), patch, 'discount.update', req);
549
+ return { status: 200, body: discountView({ ...updated, type: 'discount' }) };
550
+ }
551
+ if (seg.length === 2 && method === 'DELETE') {
552
+ const d = getRow('discount', idAt(1), req.root);
553
+ if (!d) return notFound('Discount not found.');
554
+ await write('discount', idAt(1), { _deleted: true }, 'discount.delete', req);
555
+ return { status: 204, body: null };
556
+ }
557
+ }
558
+
559
+ // ── METERS ──
560
+ if (seg[0] === 'meters') {
561
+ if (seg.length === 1 && method === 'POST') {
562
+ if (!body.name) return validationError(['name'], 'Field required', 'missing');
563
+ const id = mintId('meter', req);
564
+ const m = await write('meter', id, {
565
+ created_at: nowIso(req.occurredAt), modified_at: null,
566
+ name: body.name, aggregation: body.aggregation ?? { func: 'count' },
567
+ filter: body.filter ?? { conjunction: 'and', clauses: [] }, metadata: body.metadata ?? {},
568
+ }, 'meter.create', req);
569
+ return { status: 201, body: meterView({ ...m, type: 'meter' }) };
570
+ }
571
+ if (seg.length === 1 && method === 'GET') {
572
+ return { status: 200, body: listEnvelope(rows('meter', req.root).map((m) => meterView({ ...m, type: 'meter' }))) };
573
+ }
574
+ if (seg.length === 2 && method === 'GET') {
575
+ const m = getRow('meter', idAt(1), req.root);
576
+ return m ? { status: 200, body: meterView({ ...m, type: 'meter' }) } : notFound('Meter not found.');
577
+ }
578
+ }
579
+
580
+ // ── EVENTS ──
581
+ if (seg[0] === 'events') {
582
+ // POST /events/ingest
583
+ if (seg.length === 2 && seg[1] === 'ingest' && method === 'POST') {
584
+ const events = Array.isArray(body.events) ? (body.events as Array<Record<string, unknown>>) : null;
585
+ if (!events) return validationError(['events'], 'Field required', 'missing');
586
+ let inserted = 0;
587
+ for (const e of events) {
588
+ if (!e.name) return validationError(['events', 'name'], 'Field required', 'missing');
589
+ const id = mintId('event', req);
590
+ await write('event', id, {
591
+ timestamp: e.timestamp ?? nowIso(req.occurredAt), created_at: nowIso(req.occurredAt),
592
+ name: e.name, customer_id: e.customer_id ?? null,
593
+ external_customer_id: e.external_customer_id ?? null,
594
+ source: 'user', metadata: e.metadata ?? {},
595
+ }, 'event.ingest', req);
596
+ inserted += 1;
597
+ }
598
+ return { status: 200, body: { inserted, duplicates: 0 } };
599
+ }
600
+ if (seg.length === 1 && method === 'GET') {
601
+ const customerId = query.customer_id;
602
+ const name = query.name;
603
+ const items = rows('event', req.root)
604
+ .filter((e) => (!customerId || e.customer_id === customerId) && (!name || e.name === name))
605
+ .map((e) => eventView({ ...e, type: 'event' }));
606
+ return { status: 200, body: listEnvelope(items) };
607
+ }
608
+ if (seg.length === 2 && method === 'GET') {
609
+ const e = getRow('event', idAt(1), req.root);
610
+ return e ? { status: 200, body: eventView({ ...e, type: 'event' }) } : notFound('Event not found.');
611
+ }
612
+ }
613
+
614
+ // ── CUSTOMER SESSIONS ──
615
+ if (seg[0] === 'customer-sessions') {
616
+ if (seg.length === 1 && method === 'POST') {
617
+ const customerId = body.customer_id ? String(body.customer_id) : null;
618
+ const extId = body.external_customer_id ? String(body.external_customer_id) : null;
619
+ let cust = customerId ? getRow('customer', customerId, req.root) : undefined;
620
+ if (!cust && extId) cust = rows('customer', req.root).find((c) => c.external_id === extId);
621
+ if (!cust) return notFound('Customer not found.');
622
+ const id = mintId('customer_session', req, String(cust.id));
623
+ const token = `polar_cst_${mintSecret('customer_session_token', req, String(cust.id))}`;
624
+ const s = await write('customer_session', id, {
625
+ created_at: nowIso(req.occurredAt), modified_at: null,
626
+ token, expires_at: laterIso(req.occurredAt), customer_id: cust.id,
627
+ customer_portal_url: `https://polar.sh/twin/portal?customer_session_token=${token}`,
628
+ }, 'customer_session.create', req);
629
+ const v = view({ ...s, type: 'customer_session' });
630
+ return { status: 201, body: { ...v, customer: customerView({ ...cust, type: 'customer' }) } };
631
+ }
632
+ }
633
+
634
+ // ── CHECKOUTS ──
635
+ if (seg[0] === 'checkouts') {
636
+ if (seg.length === 1 && method === 'POST') {
637
+ const products = Array.isArray(body.products) ? (body.products as string[]) : (body.product_id ? [String(body.product_id)] : []);
638
+ if (products.length === 0) return validationError(['products'], 'Field required', 'missing');
639
+ const productId = products[0]!;
640
+ const product = getRow('product', productId, req.root);
641
+ const amount = product && Array.isArray(product.prices) && product.prices[0]
642
+ ? Number((product.prices[0] as Record<string, unknown>).price_amount ?? 1000)
643
+ : Number(body.amount ?? 1000);
644
+ const id = mintId('checkout', req);
645
+ const c = await write('checkout', id, {
646
+ created_at: nowIso(req.occurredAt), modified_at: null,
647
+ status: 'open', client_secret: `polar_checkout_${mintSecret('checkout_client_secret', req, id)}`,
648
+ url: `https://polar.sh/twin/checkout/${id}`, expires_at: laterIso(req.occurredAt),
649
+ success_url: body.success_url ?? null, embed_origin: body.embed_origin ?? null,
650
+ amount, currency: 'usd', product_id: productId, products,
651
+ customer_id: body.customer_id ?? null, customer_email: body.customer_email ?? null,
652
+ customer_name: body.customer_name ?? null, discount_id: body.discount_id ?? null,
653
+ metadata: body.metadata ?? {},
654
+ }, 'checkout.create', req);
655
+ return { status: 201, body: checkoutView({ ...c, type: 'checkout' }) };
656
+ }
657
+ if (seg.length === 1 && method === 'GET') {
658
+ return { status: 200, body: listEnvelope(rows('checkout', req.root).map((c) => checkoutView({ ...c, type: 'checkout' }))) };
659
+ }
660
+ if (seg.length === 2 && method === 'GET') {
661
+ const c = getRow('checkout', idAt(1), req.root);
662
+ return c ? { status: 200, body: checkoutView({ ...c, type: 'checkout' }) } : notFound('Checkout not found.');
663
+ }
664
+ // POST /checkouts/:id/confirm — confirm the checkout: create customer (if needed),
665
+ // subscription/order, and flip the checkout to confirmed (the twin "completes" payment).
666
+ if (seg.length === 3 && seg[2] === 'confirm' && method === 'POST') {
667
+ const c = getRow('checkout', idAt(1), req.root);
668
+ if (!c) return notFound('Checkout not found.');
669
+ if (c.status !== 'open') return notPermitted('This checkout has already been confirmed.');
670
+ // Resolve / create the customer.
671
+ let customerId = c.customer_id ? String(c.customer_id) : null;
672
+ const email = body.customer_email ?? c.customer_email ?? null;
673
+ if (!customerId && email) {
674
+ const existing = rows('customer', req.root).find((x) => x.email === email);
675
+ if (existing) customerId = String(existing.id);
676
+ else {
677
+ const cid = mintId('customer', req);
678
+ await write('customer', cid, {
679
+ created_at: nowIso(req.occurredAt), modified_at: null, email,
680
+ name: body.customer_name ?? c.customer_name ?? null, external_id: null,
681
+ customer_type: 'individual', email_verified: false, locale: 'en', metadata: {},
682
+ }, 'customer.create', req);
683
+ customerId = cid;
684
+ }
685
+ }
686
+ const product = c.product_id ? getRow('product', String(c.product_id), req.root) : undefined;
687
+ const recurring = product?.recurring_interval ?? null;
688
+ const at = nowIso(req.occurredAt);
689
+ let subscriptionId: string | null = null;
690
+ if (recurring) {
691
+ subscriptionId = mintId('subscription', req);
692
+ await write('subscription', subscriptionId, {
693
+ created_at: at, modified_at: null, status: 'active',
694
+ amount: c.amount ?? 0, currency: 'usd', recurring_interval: recurring,
695
+ current_period_start: at, current_period_end: laterIso(req.occurredAt),
696
+ started_at: at, cancel_at_period_end: false, canceled_at: null,
697
+ customer_id: customerId, product_id: c.product_id, checkout_id: c.id, metadata: {},
698
+ }, 'subscription.create', req);
699
+ }
700
+ const orderId = mintId('order', req);
701
+ await write('order', orderId, {
702
+ created_at: at, modified_at: null, status: 'paid', paid: true,
703
+ amount: c.amount ?? 0, currency: 'usd', billing_reason: recurring ? 'subscription_create' : 'purchase',
704
+ customer_id: customerId, product_id: c.product_id, subscription_id: subscriptionId,
705
+ checkout_id: c.id, metadata: {},
706
+ }, 'order.create', req);
707
+ await write('checkout', idAt(1), { status: 'confirmed', modified_at: at, customer_id: customerId, subscription_id: subscriptionId }, 'checkout.confirm', req);
708
+ const updated = getRow('checkout', idAt(1), req.root)!;
709
+ return { status: 200, body: checkoutView({ ...updated, type: 'checkout' }) };
710
+ }
711
+ }
712
+
713
+ // ── SUBSCRIPTIONS ──
714
+ if (seg[0] === 'subscriptions') {
715
+ if (seg.length === 1 && method === 'GET') {
716
+ const customerId = query.customer_id;
717
+ const items = rows('subscription', req.root)
718
+ .filter((s) => !customerId || s.customer_id === customerId)
719
+ .map((s) => subscriptionView(s, req.root));
720
+ return { status: 200, body: listEnvelope(items) };
721
+ }
722
+ if (seg.length === 2 && method === 'GET') {
723
+ const s = getRow('subscription', idAt(1), req.root);
724
+ return s ? { status: 200, body: subscriptionView(s, req.root) } : notFound('Subscription not found.');
725
+ }
726
+ if (seg.length === 2 && method === 'PATCH') {
727
+ const s = getRow('subscription', idAt(1), req.root);
728
+ if (!s) return notFound('Subscription not found.');
729
+ const patch: Record<string, unknown> = { modified_at: nowIso(req.occurredAt) };
730
+ if ('product_id' in body) patch.product_id = body.product_id;
731
+ if ('cancel_at_period_end' in body) {
732
+ patch.cancel_at_period_end = Boolean(body.cancel_at_period_end);
733
+ patch.canceled_at = body.cancel_at_period_end ? nowIso(req.occurredAt) : null;
734
+ }
735
+ if ('metadata' in body) patch.metadata = body.metadata;
736
+ if ('discount_id' in body) patch.discount_id = body.discount_id;
737
+ const updated = await write('subscription', idAt(1), patch, 'subscription.update', req);
738
+ return { status: 200, body: subscriptionView({ ...updated, customer_id: s.customer_id }, req.root) };
739
+ }
740
+ // DELETE /subscriptions/:id — cancel (immediately, or revoke). Polar returns the canceled sub.
741
+ if (seg.length === 2 && method === 'DELETE') {
742
+ const s = getRow('subscription', idAt(1), req.root);
743
+ if (!s) return notFound('Subscription not found.');
744
+ const at = nowIso(req.occurredAt);
745
+ const updated = await write('subscription', idAt(1), { status: 'canceled', canceled_at: at, ended_at: at, ends_at: at, modified_at: at }, 'subscription.cancel', req);
746
+ return { status: 200, body: subscriptionView({ ...updated, customer_id: s.customer_id }, req.root) };
747
+ }
748
+ }
749
+
750
+ // ── ORDERS ──
751
+ if (seg[0] === 'orders') {
752
+ if (seg.length === 1 && method === 'GET') {
753
+ const customerId = query.customer_id;
754
+ const subscriptionId = query.subscription_id;
755
+ const items = rows('order', req.root)
756
+ .filter((o) => (!customerId || o.customer_id === customerId) && (!subscriptionId || o.subscription_id === subscriptionId))
757
+ .map((o) => orderView({ ...o, type: 'order' }));
758
+ return { status: 200, body: listEnvelope(items) };
759
+ }
760
+ if (seg.length === 2 && method === 'GET') {
761
+ const o = getRow('order', idAt(1), req.root);
762
+ return o ? { status: 200, body: orderView({ ...o, type: 'order' }) } : notFound('Order not found.');
763
+ }
764
+ // POST /orders/:id/refund — refund an order (the twin marks it refunded offline)
765
+ if (seg.length === 3 && seg[2] === 'refund' && method === 'POST') {
766
+ const o = getRow('order', idAt(1), req.root);
767
+ if (!o) return notFound('Order not found.');
768
+ const amount = Number(body.amount ?? o.total_amount ?? o.amount ?? 0);
769
+ const updated = await write('order', idAt(1), { status: 'refunded', paid: false, refunded_amount: amount, modified_at: nowIso(req.occurredAt) }, 'order.refund', req);
770
+ return { status: 200, body: orderView({ ...updated, type: 'order' }) };
771
+ }
772
+ }
773
+
774
+ // ── WEBHOOK ENDPOINTS ──
775
+ if (seg[0] === 'webhooks' && seg[1] === 'endpoints') {
776
+ if (seg.length === 2 && method === 'POST') {
777
+ if (!body.url) return validationError(['url'], 'Field required', 'missing');
778
+ const id = mintId('webhook_endpoint', req);
779
+ const w = await write('webhook_endpoint', id, {
780
+ created_at: nowIso(req.occurredAt), modified_at: null,
781
+ url: body.url, format: body.format ?? 'raw', events: Array.isArray(body.events) ? body.events : [],
782
+ }, 'webhook_endpoint.create', req);
783
+ return { status: 201, body: webhookView({ ...w, type: 'webhook_endpoint' }) };
784
+ }
785
+ if (seg.length === 2 && method === 'GET') {
786
+ return { status: 200, body: listEnvelope(rows('webhook_endpoint', req.root).map((w) => webhookView({ ...w, type: 'webhook_endpoint' }))) };
787
+ }
788
+ if (seg.length === 3 && method === 'GET') {
789
+ const w = getRow('webhook_endpoint', idAt(2), req.root);
790
+ return w ? { status: 200, body: webhookView({ ...w, type: 'webhook_endpoint' }) } : notFound('Webhook endpoint not found.');
791
+ }
792
+ if (seg.length === 3 && method === 'DELETE') {
793
+ const w = getRow('webhook_endpoint', idAt(2), req.root);
794
+ if (!w) return notFound('Webhook endpoint not found.');
795
+ await write('webhook_endpoint', idAt(2), { _deleted: true }, 'webhook_endpoint.delete', req);
796
+ return { status: 204, body: null };
797
+ }
798
+ }
799
+
800
+ // Unmodeled route → Polar's ResourceNotFound envelope (D2 — never fabricated success).
801
+ return notFound(`Cannot ${method} ${path}`);
802
+ }
803
+
804
+ export type PolarTwinSnapshot = {
805
+ resourceTypes: readonly PolarResourceType[];
806
+ implementedEndpoints: readonly string[];
807
+ };
808
+
809
+ // Endpoint inventory used by the conformance snapshot.
810
+ export function polarTwinSnapshot(): PolarTwinSnapshot {
811
+ return {
812
+ resourceTypes: POLAR_RESOURCE_TYPES,
813
+ implementedEndpoints: [
814
+ 'POST /v1/customers/', 'GET /v1/customers/', 'GET /v1/customers/:id',
815
+ 'PATCH /v1/customers/:id', 'DELETE /v1/customers/:id', 'GET /v1/customers/:id/state',
816
+ 'POST /v1/products/', 'GET /v1/products/', 'GET /v1/products/:id', 'PATCH /v1/products/:id',
817
+ 'POST /v1/benefits/', 'GET /v1/benefits/', 'GET /v1/benefits/:id', 'DELETE /v1/benefits/:id',
818
+ 'POST /v1/discounts/', 'GET /v1/discounts/', 'GET /v1/discounts/:id', 'PATCH /v1/discounts/:id', 'DELETE /v1/discounts/:id',
819
+ 'POST /v1/meters/', 'GET /v1/meters/', 'GET /v1/meters/:id',
820
+ 'POST /v1/events/ingest', 'GET /v1/events/', 'GET /v1/events/:id',
821
+ 'POST /v1/customer-sessions/',
822
+ 'POST /v1/checkouts/', 'GET /v1/checkouts/', 'GET /v1/checkouts/:id', 'POST /v1/checkouts/:id/confirm',
823
+ 'GET /v1/subscriptions/', 'GET /v1/subscriptions/:id', 'PATCH /v1/subscriptions/:id', 'DELETE /v1/subscriptions/:id',
824
+ 'GET /v1/orders/', 'GET /v1/orders/:id', 'POST /v1/orders/:id/refund',
825
+ 'POST /v1/webhooks/endpoints/', 'GET /v1/webhooks/endpoints/', 'GET /v1/webhooks/endpoints/:id', 'DELETE /v1/webhooks/endpoints/:id',
826
+ ],
827
+ };
828
+ }