@volter/twin-polar 2.0.36 → 3.0.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.
Files changed (121) hide show
  1. package/README.md +24 -27
  2. package/dist/generated/pack-facts.json +69 -0
  3. package/dist/journeys/coverage.json +223 -0
  4. package/dist/journeys/customer-life.json +1222 -0
  5. package/dist/journeys/customer-life.outline.md +69 -0
  6. package/dist/journeys/customer-life.outline.verdict.json +41 -0
  7. package/dist/journeys/customer-life.verdict.json +71 -0
  8. package/dist/journeys/decisions.json +149 -0
  9. package/dist/journeys/demand.json +45 -0
  10. package/dist/journeys/score.json +249 -0
  11. package/dist/journeys/unreachable.json +13 -0
  12. package/dist/journeys/vendor-examples.json +9621 -0
  13. package/dist/journeys/vendor-examples.verdict.json +33 -0
  14. package/dist/spec/SOURCE.md +25 -0
  15. package/dist/spec/doc-examples.json +681 -0
  16. package/dist/spec/openapi.json.gz +0 -0
  17. package/dist/spec/sources.json +71 -0
  18. package/dist/src/cli.js +7 -16
  19. package/dist/src/fetch.d.ts +2 -0
  20. package/dist/src/fetch.js +24 -0
  21. package/dist/src/generated/surface.gen.json +1 -0
  22. package/dist/src/generated/ui.gen.json +1 -0
  23. package/dist/src/index.d.ts +5 -9
  24. package/dist/src/index.js +10 -55
  25. package/dist/src/manifest.d.ts +2 -0
  26. package/dist/src/manifest.js +123 -0
  27. package/dist/src/screens/checkout.d.ts +2 -0
  28. package/dist/src/screens/checkout.js +25 -0
  29. package/dist/src/screens/portal.d.ts +2 -0
  30. package/dist/src/screens/portal.js +18 -0
  31. package/dist/src/semantics/around.d.ts +2 -0
  32. package/dist/src/semantics/around.js +25 -0
  33. package/dist/src/semantics/checkouts.d.ts +14 -0
  34. package/dist/src/semantics/checkouts.js +131 -0
  35. package/dist/src/semantics/clock.d.ts +2 -0
  36. package/dist/src/semantics/clock.js +22 -0
  37. package/dist/src/semantics/customer-sessions.d.ts +2 -0
  38. package/dist/src/semantics/customer-sessions.js +12 -0
  39. package/dist/src/semantics/customers.d.ts +9 -0
  40. package/dist/src/semantics/customers.js +61 -0
  41. package/dist/src/semantics/doors.d.ts +11 -0
  42. package/dist/src/semantics/doors.js +47 -0
  43. package/dist/src/semantics/events.d.ts +12 -0
  44. package/dist/src/semantics/events.js +60 -0
  45. package/dist/src/semantics/index.d.ts +2 -0
  46. package/dist/src/semantics/index.js +27 -0
  47. package/dist/src/semantics/orders.d.ts +4 -0
  48. package/dist/src/semantics/orders.js +11 -0
  49. package/dist/src/semantics/products.d.ts +5 -0
  50. package/dist/src/semantics/products.js +43 -0
  51. package/dist/src/semantics/shared.d.ts +82 -0
  52. package/dist/src/semantics/shared.js +327 -0
  53. package/dist/src/semantics/states.d.ts +27 -0
  54. package/dist/src/semantics/states.js +43 -0
  55. package/dist/src/semantics/subscriptions.d.ts +4 -0
  56. package/dist/src/semantics/subscriptions.js +15 -0
  57. package/dist/src/semantics/tenant.d.ts +2 -0
  58. package/dist/src/semantics/tenant.js +2 -0
  59. package/dist/src/semantics/webhooks.d.ts +4 -0
  60. package/dist/src/semantics/webhooks.js +30 -0
  61. package/dist/src/server.d.ts +7 -0
  62. package/dist/src/server.js +7 -0
  63. package/generated/pack-facts.json +69 -0
  64. package/journeys/coverage.json +223 -0
  65. package/journeys/customer-life.json +1222 -0
  66. package/journeys/customer-life.outline.md +69 -0
  67. package/journeys/customer-life.outline.verdict.json +41 -0
  68. package/journeys/customer-life.verdict.json +71 -0
  69. package/journeys/decisions.json +149 -0
  70. package/journeys/demand.json +45 -0
  71. package/journeys/score.json +249 -0
  72. package/journeys/unreachable.json +13 -0
  73. package/journeys/vendor-examples.json +9621 -0
  74. package/journeys/vendor-examples.verdict.json +33 -0
  75. package/package.json +19 -31
  76. package/spec/SOURCE.md +25 -0
  77. package/spec/doc-examples.json +681 -0
  78. package/spec/openapi.json.gz +0 -0
  79. package/spec/sources.json +71 -0
  80. package/src/cli.ts +7 -14
  81. package/src/fetch.ts +25 -0
  82. package/src/generated/surface.gen.json +1 -0
  83. package/src/generated/ui.gen.json +1 -0
  84. package/src/index.ts +11 -85
  85. package/src/manifest.ts +135 -0
  86. package/src/screens/checkout.ts +33 -0
  87. package/src/screens/portal.ts +20 -0
  88. package/src/semantics/around.ts +25 -0
  89. package/src/semantics/checkouts.ts +133 -0
  90. package/src/semantics/clock.ts +24 -0
  91. package/src/semantics/customer-sessions.ts +13 -0
  92. package/src/semantics/customers.ts +68 -0
  93. package/src/semantics/doors.ts +51 -0
  94. package/src/semantics/events.ts +62 -0
  95. package/src/semantics/index.ts +31 -0
  96. package/src/semantics/orders.ts +15 -0
  97. package/src/semantics/products.ts +45 -0
  98. package/src/semantics/shared.ts +352 -0
  99. package/src/semantics/states.ts +50 -0
  100. package/src/semantics/subscriptions.ts +18 -0
  101. package/src/semantics/tenant.ts +5 -0
  102. package/src/semantics/webhooks.ts +29 -0
  103. package/src/server.ts +8 -0
  104. package/dist/src/polar-budget.d.ts +0 -83
  105. package/dist/src/polar-budget.js +0 -413
  106. package/dist/src/polar-capabilities.d.ts +0 -4
  107. package/dist/src/polar-capabilities.js +0 -389
  108. package/dist/src/polar-conformance.d.ts +0 -7
  109. package/dist/src/polar-conformance.js +0 -25
  110. package/dist/src/polar-connector.d.ts +0 -113
  111. package/dist/src/polar-connector.js +0 -146
  112. package/dist/src/polar-server.d.ts +0 -14
  113. package/dist/src/polar-server.js +0 -52
  114. package/dist/src/polar-twin.d.ts +0 -24
  115. package/dist/src/polar-twin.js +0 -941
  116. package/src/polar-budget.ts +0 -459
  117. package/src/polar-capabilities.ts +0 -448
  118. package/src/polar-conformance.ts +0 -33
  119. package/src/polar-connector.ts +0 -172
  120. package/src/polar-server.ts +0 -57
  121. package/src/polar-twin.ts +0 -938
@@ -0,0 +1,51 @@
1
+ // The World's doors (docs/contributing/architecture.md, "Doors, screens and the gap"), each declared in the manifest's
2
+ // `doors`: the organization's access tokens, as its settings page makes them (the API makes none), and the webhooks
3
+ // the World sent the application's server, as that server received them.
4
+ import { sha256, type HandlerContext } from '@volter/world-core';
5
+ import { bodyOf, environment, secret } from './shared.ts';
6
+
7
+ /** An Organization Access Token as the settings page makes it (`polar_oat_…`, shown once): its name, scopes and
8
+ * expiration (none: it does not expire), kept by its SHA-256. */
9
+ // source: spec:/components/securitySchemes/oat "You can generate an **Organization Access Token** from your organization's settings."
10
+ async function makeToken(ctx: HandlerContext, comment: string, expiresInDays: number | null): Promise<{ token: string; id: string }> {
11
+ const id = ctx.mint('_access_token');
12
+ const token = await secret(ctx, 'polar_oat_', `access-token:${id}`);
13
+ await ctx.record('_access_token', { comment, _environment: environment(ctx), _sha256: sha256(token), created_at: ctx.occurredAt, expires_at: expiresInDays === null ? null : new Date(Date.parse(ctx.occurredAt) + expiresInDays * 86_400_000).toISOString(), deleted: false }, id);
14
+ return { token, id };
15
+ }
16
+
17
+ /** POST /_twin/access-tokens {comment, expires_in_days?}: an Organization Access Token made on the settings page. */
18
+ export async function accessTokens(ctx: HandlerContext): Promise<Response> {
19
+ const b = bodyOf(ctx);
20
+ const days = typeof b.expires_in_days === 'number' ? b.expires_in_days : null;
21
+ return Response.json(await makeToken(ctx, typeof b.comment === 'string' ? b.comment : 'token', days), { status: 201 });
22
+ }
23
+
24
+ /** DELETE /_twin/access-tokens/{n}: a token revoked on the settings page. */
25
+ export async function revokeToken(ctx: HandlerContext): Promise<Response> {
26
+ const id = String(ctx.call.params.n);
27
+ if (!ctx.rowsRaw('_access_token').some((t) => t.id === id && t.deleted !== true)) return Response.json({ error: 'no such token' }, { status: 404 });
28
+ await ctx.record('_access_token', { deleted: true }, id);
29
+ return new Response(null, { status: 204 });
30
+ }
31
+
32
+ /** POST /_twin/app-credentials: the access token an application holds, as the runtime issues it (the descriptor's
33
+ * credentialDoor). */
34
+ export async function appCredentials(ctx: HandlerContext): Promise<Response> {
35
+ // the application's one token, made the first time the runtime asks and the same at every boot after
36
+ const token = await secret(ctx, 'polar_oat_', `access-token:app:${environment(ctx)}`);
37
+ if (!ctx.rowsRaw('_access_token', { withDeleted: true }).some((t) => t.id === `oat_app_${environment(ctx)}` && t._sha256 === sha256(token))) {
38
+ await ctx.record('_access_token', { comment: 'world', _environment: environment(ctx), _sha256: sha256(token), created_at: ctx.occurredAt, expires_at: null, deleted: false }, `oat_app_${environment(ctx)}`);
39
+ }
40
+ return Response.json({ access_token: token }, { status: 201 });
41
+ }
42
+
43
+ /** GET /_twin/deliveries[?to=<url>][&type=<event>]: every webhook Polar sent (the kernel's record of each delivery, in
44
+ * the order sent): what the application's server was told, and when. */
45
+ export async function deliveries(ctx: HandlerContext): Promise<Response> {
46
+ const q = new URL(ctx.call.request.url).searchParams;
47
+ const rows = ctx.rowsRaw('_webhook_delivery')
48
+ .filter((d) => (q.get('to') === null || d.url === q.get('to')) && (q.get('type') === null || d.event_type === q.get('type')))
49
+ .sort((a, b) => Number(a.sent_at) - Number(b.sent_at));
50
+ return Response.json({ data: rows.map((d) => ({ url: d.url, type: d.event_type, sent_at: d.sent_at, headers: d.headers, body: JSON.parse(String(d.body)) })) });
51
+ }
@@ -0,0 +1,62 @@
1
+ // Usage events (the `events` family: ingested by the application, read by meters) and, as the manifest's `events.render`,
2
+ // how Polar renders a webhook's `data`: the written object as the API answers it.
3
+ import type { EventWrite, HandlerContext, WriteHookContext } from '@volter/world-core';
4
+ import { rowOf,
5
+ admits, among, bodyOf, customerFields, eventFields, environment, CHECKOUT, checkoutView, CUSTOMER, customerBy, customerView, EVENT, eventView, invalid, METER, missing, newest, newId, notFound, ORDER, orderView,
6
+ PRODUCT, productView, type Row, rowsOf, SUBSCRIPTION, subscriptionView,
7
+ } from './shared.ts';
8
+
9
+ /** `POST /v1/events/ingest`: a batch of events. An event's `external_id` is the application's id for it: a second
10
+ * ingest of it is a duplicate, not a second event. An `external_customer_id` names the customer by the application's
11
+ * id; it does not create an unresolved customer. */
12
+ // source: spec:events:ingest "Ingest batch of events."
13
+ export async function events_ingest(ctx: HandlerContext): Promise<Response> {
14
+ const events = bodyOf(ctx).events;
15
+ if (!Array.isArray(events)) return missing('events');
16
+ // Validate the complete batch before mutating it. External identities may remain unresolved.
17
+ // source: spec:events:ingest "Validation Error"
18
+ for (const [i, e] of (events as Row[]).entries()) {
19
+ if (!e || typeof e.name !== 'string') return invalid(['events', i, 'name'], 'Field required', 'missing');
20
+ if (e.customer_id !== undefined && !customerBy(ctx, { id: e.customer_id })) return invalid(['events', i, 'customer_id'], 'Customer does not exist.');
21
+ }
22
+ let inserted = 0;
23
+ let duplicates = 0;
24
+ for (const e of events as Row[]) {
25
+ if (e.external_id != null && rowsOf(ctx, '_event_key').some((x) => x.external_id === e.external_id)) { duplicates += 1; continue; }
26
+ const customer = e.customer_id !== undefined ? customerBy(ctx, { id: e.customer_id }) : e.external_customer_id !== undefined ? customerBy(ctx, { external: e.external_customer_id }) : undefined;
27
+ const id = newId(ctx, EVENT);
28
+ await ctx.write(EVENT, id, { ...eventFields(ctx, { id, timestamp: e.timestamp ?? ctx.occurredAt, name: e.name, customer_id: customer?.id ?? null, external_customer_id: e.external_customer_id ?? null, metadata: e.metadata ?? {} }), _environment: environment(ctx), _received_at: ctx.occurredAt }, 'event.ingest');
29
+ if (e.external_id != null) await ctx.record('_event_key', { external_id: e.external_id, _environment: environment(ctx) });
30
+ inserted += 1;
31
+ }
32
+ return Response.json({ inserted, duplicates });
33
+ }
34
+
35
+ /** `GET /v1/events/`: the organization's events, newest first, by customer (Polar's id or the application's), name,
36
+ * source, a meter's filter, or a time window. */
37
+ // source: spec:events:list "List events."
38
+ export async function events_list(ctx: HandlerContext): Promise<Response> {
39
+ const meter = ctx.params.meter_id === undefined ? undefined : rowOf(ctx, METER, String(ctx.params.meter_id));
40
+ if (ctx.params.meter_id !== undefined && (!meter || meter.deleted === true)) return notFound('Meter');
41
+ // source: spec:events:list "Filter events after this timestamp."
42
+ const after = ctx.params.start_timestamp === undefined ? undefined : Date.parse(String(ctx.params.start_timestamp));
43
+ const before = ctx.params.end_timestamp === undefined ? undefined : Date.parse(String(ctx.params.end_timestamp));
44
+ const q = String(ctx.params.query ?? '').toLowerCase();
45
+ const rows = rowsOf(ctx, EVENT)
46
+ .filter((e) => among(ctx, 'customer_id', e.customer_id) && among(ctx, 'external_customer_id', e.external_customer_id) && among(ctx, 'name', e.name) && among(ctx, 'source', 'user'))
47
+ .filter((e) => !meter || admits(meter.filter as Row | undefined, e))
48
+ .filter((e) => (after === undefined || Date.parse(String(e.timestamp)) > after) && (before === undefined || Date.parse(String(e.timestamp)) < before))
49
+ .filter((e) => q === '' || String(e.name).toLowerCase().includes(q))
50
+ .sort(newest('timestamp'));
51
+ return ctx.list(EVENT, rows.map((e) => eventView(ctx, e)));
52
+ }
53
+
54
+ /** A webhook's `data`: the written object as the API answers it. */
55
+ export function data(ctx: WriteHookContext, write: EventWrite): Record<string, unknown> {
56
+ const id = String(write.body.id ?? '');
57
+ const row = rowOf(ctx, write.storedType, id) ?? write.body;
58
+ return write.storedType === CHECKOUT ? checkoutView(ctx, row) : orderView(ctx, row);
59
+ }
60
+
61
+ /** Private routing value: sandbox endpoints only receive sandbox writes. */
62
+ export function values(ctx: WriteHookContext): Record<string, unknown> { return { '$environment': environment(ctx) }; }
@@ -0,0 +1,31 @@
1
+ // GENERATED by scripts/create-pack.ts --index: the handler map, by operationId, from the families' files (each
2
+ // handler named as its operation). Do not edit; add a handler to its family's file and generate again.
3
+ import type { Handler } from '@volter/world-core';
4
+ import * as checkouts from './checkouts.ts';
5
+ import * as customer_sessions from './customer-sessions.ts';
6
+ import * as customers from './customers.ts';
7
+ import * as events from './events.ts';
8
+ import * as orders from './orders.ts';
9
+ import * as products from './products.ts';
10
+ import * as subscriptions from './subscriptions.ts';
11
+ import * as webhooks from './webhooks.ts';
12
+
13
+ export const polarSemantics: Record<string, Handler> = {
14
+ "checkouts:client_confirm": checkouts.checkouts_client_confirm,
15
+ "checkouts:client_get": checkouts.checkouts_client_get,
16
+ "checkouts:create": checkouts.checkouts_create,
17
+ "checkouts:get": checkouts.checkouts_get,
18
+ "checkouts:list": checkouts.checkouts_list,
19
+ "customer-sessions:create": customer_sessions.customer_sessions_create,
20
+ "customers:create": customers.customers_create,
21
+ "customers:get": customers.customers_get,
22
+ "customers:get_state_external": customers.customers_get_state_external,
23
+ "customers:list": customers.customers_list,
24
+ "events:ingest": events.events_ingest,
25
+ "events:list": events.events_list,
26
+ "orders:list": orders.orders_list,
27
+ "products:create": products.products_create,
28
+ "products:list": products.products_list,
29
+ "subscriptions:list": subscriptions.subscriptions_list,
30
+ "webhooks:create_webhook_endpoint": webhooks.webhooks_create_webhook_endpoint,
31
+ };
@@ -0,0 +1,15 @@
1
+ // Orders: made when a checkout is paid (./checkouts.ts), read back by the application.
2
+ import type { HandlerContext } from '@volter/world-core';
3
+ import { ORDER, orderView, rowsOf } from './shared.ts';
4
+
5
+ const FILTERS = ['checkout_id', 'customer_id', 'product_id', 'subscription_id', 'status'];
6
+
7
+ /** `GET /v1/orders/`: the organization's orders, newest first, filtered by the checkout, customer, product,
8
+ * subscription or status named. */
9
+ // source: spec:orders:list "List orders."
10
+ export async function orders_list(ctx: HandlerContext): Promise<Response> {
11
+ const rows = rowsOf(ctx, ORDER)
12
+ .filter((o) => FILTERS.every((k) => ctx.params[k] === undefined || ([] as unknown[]).concat(ctx.params[k]).map(String).includes(String(o[k]))))
13
+ .sort((a, b) => String(b.created_at).localeCompare(String(a.created_at)));
14
+ return ctx.list('Order', rows.map((o) => orderView(ctx, o)));
15
+ }
@@ -0,0 +1,45 @@
1
+ // Products and their prices.
2
+ import type { HandlerContext } from '@volter/world-core';
3
+ import { rowOf, bodyOf, environment, productFields, priceView, invalid, missing, newId, PRODUCT, productView, type Row, rowsOf } from './shared.ts';
4
+
5
+ const AMOUNT_TYPES = new Set(['fixed', 'free', 'custom', 'metered_unit', 'seat_based']);
6
+
7
+ /** `POST /v1/products/`: a product with its prices, each given an id. */
8
+ // source: spec:products:create "Create a product."
9
+ export async function products_create(ctx: HandlerContext): Promise<Response> {
10
+ const body = bodyOf(ctx);
11
+ if (typeof body.name !== 'string' || body.name === '') return missing('name');
12
+ const prices = Array.isArray(body.prices) ? body.prices as Row[] : [];
13
+ if (prices.length === 0) return missing('prices');
14
+ const bad = prices.findIndex((p) => !AMOUNT_TYPES.has(String(p.amount_type)));
15
+ if (bad >= 0) return invalid(['prices', bad, 'amount_type'], 'Input tag does not match any of the expected tags', 'union_tag_invalid');
16
+ const id = newId(ctx, PRODUCT);
17
+ const stored: Row[] = [];
18
+ for (const price of prices) {
19
+ const priceId = ctx.mint('_price');
20
+ await ctx.record('_price', {}, priceId);
21
+ stored.push(priceView(ctx, { ...price, id: priceId }, { ...body, id, created_at: ctx.occurredAt }));
22
+ }
23
+ await ctx.write(PRODUCT, id, { ...productFields(ctx, {
24
+ id, created_at: ctx.occurredAt, modified_at: null, name: body.name, description: body.description ?? null, recurring_interval: body.recurring_interval ?? null,
25
+ recurring_interval_count: body.recurring_interval_count ?? (body.recurring_interval ? 1 : null), trial_interval: body.trial_interval ?? null,
26
+ trial_interval_count: body.trial_interval_count ?? null, visibility: body.visibility ?? 'public', is_archived: false, prices: stored, benefit_ids: [], metadata: body.metadata ?? {},
27
+ }), _environment: environment(ctx) }, 'product.create');
28
+ return Response.json(productView(ctx, rowOf(ctx, PRODUCT, id)!), { status: 201 });
29
+ }
30
+
31
+ /** `GET /v1/products/`: filtered by archived, recurring, a query over names, or a benefit they grant. */
32
+ // source: spec:products:list "List products."
33
+ export async function products_list(ctx: HandlerContext): Promise<Response> {
34
+ const flag = (v: unknown): boolean | undefined => (v === undefined ? undefined : String(v) === 'true');
35
+ const archived = flag(ctx.params.is_archived);
36
+ const recurring = flag(ctx.params.is_recurring);
37
+ const q = String(ctx.params.query ?? '').toLowerCase();
38
+ const rows = rowsOf(ctx, PRODUCT)
39
+ .filter((p) => archived === undefined || Boolean(p.is_archived) === archived)
40
+ .filter((p) => recurring === undefined || (p.recurring_interval != null) === recurring)
41
+ .filter((p) => q === '' || String(p.name).toLowerCase().includes(q))
42
+ .filter((p) => ctx.params.benefit_id === undefined || ((p.benefit_ids as string[] | undefined) ?? []).includes(String(ctx.params.benefit_id)))
43
+ .sort((a, b) => String(b.created_at).localeCompare(String(a.created_at)));
44
+ return ctx.list(PRODUCT, rows.map((p) => productView(ctx, p)));
45
+ }
@@ -0,0 +1,352 @@
1
+ // What every family of Polar shares: the World's organization, ids, the objects as the spec's schemas require them
2
+ // (the SDK parses every answer and every webhook with them), a customer's meters, and Polar's answers.
3
+ import type { HandlerContext, WriteHookContext } from '@volter/world-core';
4
+
5
+ export type Row = Record<string, unknown>;
6
+ type Reader = Pick<HandlerContext, 'row' | 'rowsRaw' | 'crypto' | 'own' | 'call'> | Pick<WriteHookContext, 'row' | 'rowsRaw' | 'crypto' | 'own' | 'call'>;
7
+
8
+ /** A customer by its id or its external id. */
9
+ export function customerBy(ctx: Reader, key: { id?: unknown; external?: unknown }): Row | undefined {
10
+ return key.id !== undefined ? rowsOf(ctx, CUSTOMER).find((c) => c.id === key.id) : rowsOf(ctx, CUSTOMER).find((c) => c.external_id === key.external);
11
+ }
12
+
13
+ export const ORGANIZATION = 'organization';
14
+ export const CUSTOMER = 'customer';
15
+ export const PRODUCT = 'product';
16
+ export const CHECKOUT = 'checkout';
17
+ export const SUBSCRIPTION = 'subscription';
18
+ export const ORDER = 'order';
19
+ export const EVENT = 'event';
20
+ export const METER = 'meter';
21
+ export const SESSION = 'customer_session';
22
+ export const ENDPOINT = 'webhook_endpoint';
23
+
24
+ /** Where Polar's hosted pages are: the checkout (`buy.polar.sh`) and the portal (`polar.sh`), rewritten to the twin. */
25
+ export const CHECKOUT_ORIGIN = 'https://buy.polar.sh';
26
+ export const PORTAL_ORIGIN = 'https://polar.sh';
27
+
28
+ // ── answers ────────────────────────────────────────────────────────────────────────────────────
29
+ // source: spec:/components/schemas/ResourceNotFound "ResourceNotFound"
30
+
31
+ /** Polar's error body: `{ error: <kind>, detail }`. */
32
+ export const fail = (status: number, error: string, detail: string): Response => Response.json({ error, detail }, { status });
33
+ export const notFound = (what: string): Response => fail(404, 'ResourceNotFound', `${what} not found.`);
34
+ /** FastAPI's 422, one error per field. */
35
+ // source: spec:/components/schemas/HTTPValidationError "HTTPValidationError"
36
+ export const invalid = (loc: Array<string | number>, msg: string, type = 'value_error'): Response =>
37
+ Response.json({ detail: [{ type, loc: ['body', ...loc], msg, input: null }] }, { status: 422 });
38
+ export const missing = (field: string): Response => invalid([field], 'Field required', 'missing');
39
+
40
+ export const bodyOf = (ctx: HandlerContext): Row => (ctx.body && typeof ctx.body === 'object' && !Array.isArray(ctx.body) ? ctx.body as Row : {});
41
+
42
+ // ── ids and the organization ───────────────────────────────────────────────────────────────────
43
+
44
+ /** A new subject's id: a UUID the kernel mints from the tree (ctx.mint), under the resource the manifest declares for its
45
+ * stored type. */
46
+ const DECLARED: Record<string, string> = { checkout: 'Checkout', subscription: 'Subscription', order: 'Order', webhook_endpoint: 'WebhookEndpoint' };
47
+ export const newId = (ctx: Pick<HandlerContext, 'mint'>, type: string): string => ctx.mint(DECLARED[type] ?? type);
48
+
49
+ /** A secret of Polar's form (`polar_cst_…`, `polar_c_…`, `polar_whs_…`, `polar_oat_…`): its prefix and 43 letters and
50
+ * digits drawn from a secret the World holds for the seed (ctx.secret), so none is computed from the ids it names. */
51
+ export async function secret(ctx: Pick<HandlerContext, 'secret'>, prefix: string, seed: string): Promise<string> {
52
+ const a = await ctx.secret(`polar-secret:${seed}`);
53
+ const b = await ctx.secret(`polar-secret:${seed}:2`);
54
+ return `${prefix}${`${a}${b}`.replace(/[^A-Za-z0-9]/g, '').slice(0, 43)}`;
55
+ }
56
+
57
+ /** The World's one organization: the one its token names. */
58
+ export const ORG_ID = '1dbfc517-0bbf-4301-9ba8-555ca42b9737';
59
+ export const orgId = (ctx: Pick<HandlerContext, 'call'>): string => environment(ctx) === 'sandbox' ? '2dbfc517-0bbf-4301-9ba8-555ca42b9737' : ORG_ID;
60
+ export const organizationView = (ctx: Reader): Row => {
61
+ const o = rowOf(ctx, ORGANIZATION, 'org') ?? {};
62
+ return {
63
+ created_at: o.created_at ?? '2026-01-01T00:00:00Z', modified_at: null, id: orgId(ctx), name: o.name ?? 'Twin organization', slug: o.slug ?? 'twin',
64
+ avatar_url: null, proration_behavior: 'prorate', allow_customer_updates: true, email: null, website: null, socials: [], status: 'active',
65
+ details_submitted_at: null, onboarding_resubmission_requested_at: null, sso_enforced: false, default_presentment_currency: 'usd',
66
+ default_tax_behavior: 'location', feature_settings: {}, subscription_settings: { allow_multiple_subscriptions: false, allow_customer_updates: true, proration_behavior: 'prorate', benefit_revocation_grace_period: 0, prevent_trial_abuse: false },
67
+ customer_email_settings: {}, customer_portal_settings: {}, dispute_settings: {}, embed_hosts: [], embed_hosts_enforced: false, account_id: null, payout_account_id: null, capabilities: {},
68
+ };
69
+ };
70
+
71
+ // ── views: each object with every field its schema requires ───────────────────────────────────
72
+
73
+ /** Whether a row's field is among the values a list's query names (one value or several), or the query names none. */
74
+ export const among = (ctx: Pick<HandlerContext, 'params'>, key: string, value: unknown): boolean =>
75
+ ctx.params[key] === undefined || ([] as unknown[]).concat(ctx.params[key]).map(String).includes(String(value));
76
+ /** Newest first by a timestamp field. */
77
+ export const newest = (field: string) => (a: Row, b: Row): number => String(b[field]).localeCompare(String(a[field]));
78
+
79
+ /** Stored rows without the kernel's bookkeeping. */
80
+ export const environment = (ctx: Pick<HandlerContext, 'call'>): string => new URL(ctx.call.request.url).hostname.startsWith('sandbox') ? 'sandbox' : 'production';
81
+ export const rowsOf = (ctx: Reader, type: string): Row[] => ctx.rowsRaw(type).filter((r) => r.deleted !== true && (r._environment ?? (r.organization_id === '2dbfc517-0bbf-4301-9ba8-555ca42b9737' ? 'sandbox' : 'production')) === environment(ctx));
82
+ export const rowOf = (ctx: Reader, type: string, id: string): Row | undefined => rowsOf(ctx, type).find((r) => r.id === id);
83
+ /** The stored vendor shape; bookkeeping is private, including the environment boundary. */
84
+ export const vendorFields = (ctx: Pick<HandlerContext, 'own'>, row: Row): Row => Object.fromEntries(Object.entries(ctx.own(row)).filter(([k]) => !k.startsWith('_')));
85
+ export const customerView = (ctx: Reader, c: Row): Row => vendorFields(ctx, c);
86
+ export const productView = (ctx: Reader, p: Row): Row => vendorFields(ctx, p);
87
+ export const subscriptionView = (ctx: Reader, s: Row): Row => vendorFields(ctx, s);
88
+ export const orderView = (ctx: Reader, o: Row): Row => vendorFields(ctx, o);
89
+ export const eventView = (ctx: Reader, e: Row): Row => vendorFields(ctx, e);
90
+ export const checkoutView = (ctx: Reader, c: Row): Row => vendorFields(ctx, c);
91
+
92
+
93
+ // source: spec:/components/schemas/CustomerIndividual "CustomerIndividual"
94
+ export function customerFields(ctx: Reader, c: Row): Row {
95
+ return {
96
+ id: c.id, created_at: c.created_at, modified_at: c.modified_at ?? null, metadata: c.metadata ?? {}, external_id: c.external_id ?? null,
97
+ email: c.email, email_verified: c.email_verified ?? false, type: 'individual', name: c.name ?? null, billing_name: c.billing_name ?? null,
98
+ billing_address: c.billing_address ?? null, tax_id: c.tax_id ?? null, locale: c.locale ?? null, organization_id: orgId(ctx),
99
+ default_payment_method_id: null, deleted_at: c.deleted_at ?? null, first_user_event_at: c.first_user_event_at ?? null,
100
+ avatar_url: `https://www.gravatar.com/avatar/${String(c.email ?? '')}?d=404`,
101
+ };
102
+ }
103
+
104
+ /** A price as the schema of its amount type requires (ProductPriceFixed, ProductPriceFree, ProductPriceCustom,
105
+ * ProductPriceMeteredUnit). */
106
+ // source: spec:/components/schemas/ProductPriceFixed "ProductPriceFixed"
107
+ export function priceView(ctx: Reader, p: Row, product: Row): Row {
108
+ const base = {
109
+ created_at: product.created_at, modified_at: null, id: p.id, source: p.source ?? 'catalog', amount_type: p.amount_type, price_currency: p.price_currency ?? 'usd',
110
+ tax_behavior: null, is_archived: p.is_archived ?? false, product_id: product.id,
111
+ };
112
+ if (p.amount_type === 'fixed') return { ...base, price_amount: p.price_amount };
113
+ if (p.amount_type === 'custom') return { ...base, minimum_amount: p.minimum_amount ?? null, maximum_amount: p.maximum_amount ?? null, preset_amount: p.preset_amount ?? null };
114
+ if (p.amount_type !== 'metered_unit') return base;
115
+ const meter = rowOf(ctx, METER, String(p.meter_id));
116
+ // source: spec:/components/schemas/ProductPriceMeter "A meter associated to a metered price."
117
+ // source: spec:/components/schemas/MeterCreate/properties/unit "scalar"
118
+ return { ...base, unit_amount: String(p.unit_amount ?? '0'), cap_amount: p.cap_amount ?? null, meter_id: p.meter_id,
119
+ meter: { id: p.meter_id, name: meter?.name ?? '', unit: meter?.unit ?? 'scalar', custom_label: meter?.custom_label ?? null, custom_multiplier: meter?.custom_multiplier ?? null } };
120
+ }
121
+
122
+ // source: spec:/components/schemas/Product "Product"
123
+ export function productFields(ctx: Reader, p: Row): Row {
124
+ return {
125
+ id: p.id, created_at: p.created_at, modified_at: p.modified_at ?? null, trial_interval: p.trial_interval ?? null, trial_interval_count: p.trial_interval_count ?? null,
126
+ name: p.name, description: p.description ?? null, visibility: p.visibility ?? 'public', recurring_interval: p.recurring_interval ?? null,
127
+ recurring_interval_count: p.recurring_interval ? Number(p.recurring_interval_count ?? 1) : null, meter_interval: null, meter_interval_count: null,
128
+ is_recurring: p.recurring_interval != null, is_archived: p.is_archived ?? false, organization_id: orgId(ctx), metadata: p.metadata ?? {},
129
+ prices: p.prices ?? [],
130
+ benefits: [],
131
+ medias: [], attached_custom_fields: [],
132
+ };
133
+ }
134
+
135
+ // source: spec:/components/schemas/Subscription "Subscription"
136
+ export function subscriptionFields(ctx: Reader, s: Row): Row {
137
+ const customer = rowOf(ctx, CUSTOMER, String(s.customer_id));
138
+ const product = rowOf(ctx, PRODUCT, String(s.product_id));
139
+ return {
140
+ created_at: s.created_at, modified_at: s.modified_at ?? null, id: s.id, amount: s.amount ?? 0, currency: s.currency ?? 'usd',
141
+ recurring_interval: s.recurring_interval, recurring_interval_count: Number(s.recurring_interval_count ?? 1), status: s.status,
142
+ current_period_start: s.current_period_start, current_period_end: s.current_period_end ?? null, current_meter_period_start: null, current_meter_period_end: null,
143
+ trial_start: s.trial_start ?? null, trial_end: s.trial_end ?? null, cancel_at_period_end: s.cancel_at_period_end ?? false, canceled_at: s.canceled_at ?? null,
144
+ started_at: s.started_at ?? null, ends_at: s.ends_at ?? null, ended_at: s.ended_at ?? null, pause_at_period_end: false, paused_at: null, resumes_at: null,
145
+ customer_id: s.customer_id, product_id: s.product_id, discount_id: s.discount_id ?? null, checkout_id: s.checkout_id ?? null,
146
+ customer_cancellation_reason: s.customer_cancellation_reason ?? null, customer_cancellation_comment: s.customer_cancellation_comment ?? null,
147
+ metadata: s.metadata ?? {}, custom_field_data: {},
148
+ customer: customer ? customerView(ctx, customer) : null, product: product ? productView(ctx, product) : null, discount: null,
149
+ prices: s.prices ?? product?.prices ?? [], meters: s.meters ?? [], pending_update: null,
150
+ };
151
+ }
152
+
153
+ // source: spec:/components/schemas/Order "Order"
154
+ export function orderFields(ctx: Reader, o: Row): Row {
155
+ const customer = rowOf(ctx, CUSTOMER, String(o.customer_id));
156
+ const product = o.product_id ? rowOf(ctx, PRODUCT, String(o.product_id)) : undefined;
157
+ const sub = o.subscription_id ? rowOf(ctx, SUBSCRIPTION, String(o.subscription_id)) : undefined;
158
+ const total = Number(o.total_amount ?? 0);
159
+ const refunded = Number(o.refunded_amount ?? 0);
160
+ return {
161
+ id: o.id, created_at: o.created_at, modified_at: o.modified_at ?? null, status: o.status, paid: o.status !== 'pending',
162
+ subtotal_amount: total, discount_amount: 0, net_amount: total, tax_amount: 0, total_amount: total, applied_balance_amount: 0, due_amount: 0,
163
+ refunded_amount: refunded, refunded_tax_amount: 0, currency: o.currency ?? 'usd', billing_reason: o.billing_reason, billing_name: null, billing_address: null,
164
+ invoice_number: `TWIN-${String(o.number ?? 1).padStart(4, '0')}`, is_invoice_generated: false, receipt_number: null,
165
+ customer_id: o.customer_id, product_id: o.product_id ?? null, discount_id: null, subscription_id: o.subscription_id ?? null, checkout_id: o.checkout_id ?? null,
166
+ metadata: o.metadata ?? {}, custom_field_data: {}, platform_fee_amount: 0, platform_fee_currency: null,
167
+ customer: customer ? customerView(ctx, customer) : null,
168
+ product: product ? (({ prices: _p, benefits: _b, medias: _m, attached_custom_fields: _a, ...rest }) => rest)(productView(ctx, product)) : null,
169
+ discount: null, subscription: sub ? (({ customer: _c, product: _pr, discount: _d, prices: _ps, meters: _ms, pending_update: _u, ...rest }) => rest)(subscriptionView(ctx, sub)) : null,
170
+ items: o.items ?? [],
171
+ description: product?.name ?? 'Order', refundable_amount: total - refunded, refundable_tax_amount: 0,
172
+ };
173
+ }
174
+
175
+ /** A usage event as the API answers it (UserEvent): with its customer. */
176
+ // source: spec:/components/schemas/UserEvent "An event you created through the ingestion API."
177
+ // Where the documentation stops: a user event's `label` (its type's "human readable label"); the twin gives its name
178
+ export function eventFields(ctx: Reader, e: Row): Row {
179
+ const customer = e.customer_id ? rowOf(ctx, CUSTOMER, String(e.customer_id)) : undefined;
180
+ return {
181
+ id: e.id, timestamp: e.timestamp, organization_id: orgId(ctx), customer_id: e.customer_id ?? null, customer: customer && customer.deleted !== true ? customerView(ctx, customer) : null,
182
+ external_customer_id: e.external_customer_id ?? null, member_id: null, external_member_id: null, child_count: 0, parent_id: null,
183
+ label: e.name, name: e.name, source: 'user', metadata: e.metadata ?? {},
184
+ };
185
+ }
186
+
187
+ // source: spec:/components/schemas/Checkout "Checkout"
188
+ export function checkoutFields(ctx: Reader, c: Row): Row {
189
+ const product = rowOf(ctx, PRODUCT, String(c.product_id));
190
+ const pv = product ? productView(ctx, product) : null;
191
+ const prices = (c.prices as Record<string, Row[]> | undefined)?.[String(c.product_id)] ?? (pv?.prices as Row[] | undefined) ?? [];
192
+ const price = prices.find((p) => p.id === c.product_price_id) ?? prices[0] ?? null;
193
+ const amount = Number(c.amount ?? 0);
194
+ return {
195
+ id: c.id, created_at: c.created_at, modified_at: c.modified_at ?? null, payment_processor: 'stripe', status: c.status, client_secret: c.client_secret,
196
+ url: `${CHECKOUT_ORIGIN}/${String(c.client_secret)}`, expires_at: c.expires_at, success_url: c.success_url ?? `${CHECKOUT_ORIGIN}/${String(c.client_secret)}/confirmation`,
197
+ return_url: c.return_url ?? null, embed_origin: c.embed_origin ?? null, amount, discount_amount: 0, net_amount: amount, tax_amount: null, tax_behavior: null,
198
+ total_amount: amount, currency: c.currency ?? 'usd', allow_trial: c.allow_trial ?? null, active_trial_interval: product?.trial_interval ?? null, active_trial_interval_count: product?.trial_interval_count ?? null,
199
+ trial_end: null, organization_id: orgId(ctx), product_id: c.product_id, product_price_id: price?.id ?? null, discount_id: null, allow_discount_codes: c.allow_discount_codes ?? true,
200
+ require_billing_address: c.require_billing_address ?? false, is_discount_applicable: false, is_free_product_price: amount === 0, is_payment_required: amount > 0,
201
+ is_payment_setup_required: pv?.is_recurring === true, is_payment_form_required: amount > 0, customer_id: c.customer_id ?? null, is_business_customer: c.is_business_customer ?? false,
202
+ customer_name: c.customer_name ?? null, customer_email: c.customer_email ?? null, customer_ip_address: c.customer_ip_address ?? null, customer_billing_name: null,
203
+ customer_billing_address: null, customer_tax_id: null, payment_method_type: null, payment_processor_metadata: {},
204
+ billing_address_fields: { country: 'required', state: 'disabled', city: 'disabled', postal_code: 'disabled', line1: 'disabled', line2: 'disabled' },
205
+ trial_interval: null, trial_interval_count: null, metadata: c.metadata ?? {}, external_customer_id: c.external_customer_id ?? null, custom_field_data: {},
206
+ products: pv ? [pv] : [], product: pv, product_price: price, prices: pv ? { [String(pv.id)]: prices } : {}, discount: null,
207
+ subscription_id: c.subscription_id ?? null, attached_custom_fields: [], customer_metadata: c.customer_metadata ?? {},
208
+ };
209
+ }
210
+
211
+ // ── a customer's meters ────────────────────────────────────────────────────────────────────────
212
+
213
+ /** Whether a meter's filter admits an event: clauses on `name`, `source` or a metadata key, joined by the filter's
214
+ * conjunction, nested filters among them. */
215
+ // source: spec:/components/schemas/Filter "Filter"
216
+ export function admits(filter: Row | undefined, e: Row): boolean {
217
+ const clauses = (filter?.clauses as Row[] | undefined) ?? [];
218
+ if (clauses.length === 0) return true;
219
+ const one = (c: Row): boolean => {
220
+ if ('clauses' in c) return admits(c, e);
221
+ const prop = String(c.property ?? '');
222
+ const actual = prop === 'name' || prop === 'source' ? e[prop] : ((e.metadata as Row | undefined) ?? {})[prop.replace(/^metadata\./, '')];
223
+ const want = c.value;
224
+ switch (String(c.operator ?? 'eq')) {
225
+ case 'eq': return String(actual) === String(want);
226
+ default: return compareClause(String(c.operator), actual, want);
227
+ }
228
+ };
229
+ return String(filter?.conjunction ?? 'and') === 'or' ? clauses.some(one) : clauses.every(one);
230
+ }
231
+
232
+ /** A meter's aggregation over the events it admits: a count, or the sum, max, min or average of a metadata property. */
233
+ // source: spec:/components/schemas/PropertyAggregation "PropertyAggregation"
234
+ export function aggregate(agg: Row | undefined, events: Row[]): number {
235
+ const func = String(agg?.func ?? 'count');
236
+ if (func === 'count') return events.length;
237
+ const prop = String(agg?.property ?? '').replace(/^metadata\./, '');
238
+ // source: https://polar.sh/docs/features/usage-based-billing/meters "Counts the number of unique values of a property."
239
+ if (func === 'unique') return new Set(events.map((e) => ((e.metadata as Row | undefined) ?? {})[prop])).size;
240
+ const values = events.map((e) => Number(((e.metadata as Row | undefined) ?? {})[prop])).filter(Number.isFinite);
241
+ if (values.length === 0) return 0;
242
+ if (func === 'max') return Math.max(...values);
243
+ if (func === 'min') return Math.min(...values);
244
+ const sum = values.reduce((a, b) => a + b, 0);
245
+ return func === 'avg' ? sum / values.length : sum;
246
+ }
247
+
248
+ /** A customer's meter: consumed (the fold over its events), credited (its meter-credit grants), and the balance. */
249
+ // source: spec:/components/schemas/CustomerStateMeter "The balance of the meter, i.e. the difference between credited and consumed units."
250
+ export function customerMeter(ctx: Reader, customerId: string, meter: Row): { consumed: number; credited: number; balance: number } {
251
+ const events = rowsOf(ctx, EVENT).filter((e) => e.customer_id === customerId && admits(meter.filter as Row | undefined, e));
252
+ const consumed = aggregate(meter.aggregation as Row | undefined, events);
253
+ const credited = 0; // No demanded product grants meter credits; benefits are outside the served surface.
254
+ return { consumed, credited, balance: credited - consumed };
255
+ }
256
+
257
+ /** A customer's state: its active subscriptions, granted benefits and meters. */
258
+ // source: spec:customers:get_state "The customer state includes information about the customer's active subscriptions and benefits."
259
+ export async function customerState(ctx: HandlerContext, c: Row): Promise<Row> {
260
+ const id = String(c.id);
261
+ for (const meter of rowsOf(ctx, METER)) {
262
+ if (!rowsOf(ctx, '_customer_meter').some((r) => r.customer_id === id && r.meter_id === meter.id)) {
263
+ const meterId = ctx.mint('_customer_meter');
264
+ await ctx.record('_customer_meter', { id: meterId, customer_id: id, meter_id: meter.id, _environment: environment(ctx) }, meterId);
265
+ }
266
+ }
267
+ const subs = rowsOf(ctx, SUBSCRIPTION).filter((s) => s.customer_id === id && (s.status === 'active' || s.status === 'trialing'));
268
+ return {
269
+ ...customerView(ctx, c),
270
+ active_subscriptions: subs.map((s) => {
271
+ const v = subscriptionView(ctx, s);
272
+ return { id: v.id, created_at: v.created_at, modified_at: v.modified_at, metadata: v.metadata, status: v.status, amount: v.amount, currency: v.currency, recurring_interval: v.recurring_interval, current_period_start: v.current_period_start, current_period_end: v.current_period_end, trial_start: v.trial_start, trial_end: v.trial_end, cancel_at_period_end: v.cancel_at_period_end, canceled_at: v.canceled_at, started_at: v.started_at, ends_at: v.ends_at, product_id: v.product_id, discount_id: v.discount_id, meters: [], custom_field_data: {} };
273
+ }),
274
+ granted_benefits: [],
275
+ active_meters: rowsOf(ctx, METER).map((m) => {
276
+ const { consumed, credited, balance } = customerMeter(ctx, id, m);
277
+ return { id: rowsOf(ctx, '_customer_meter').find((r) => r.customer_id === id && r.meter_id === m.id)!.id, created_at: m.created_at, modified_at: null, meter_id: m.id, consumed_units: consumed, credited_units: credited, balance };
278
+ }),
279
+ };
280
+ }
281
+
282
+
283
+ // source: https://polar.sh/docs/features/subscriptions/introduction "creates a new order"
284
+ export async function makeOrder(ctx: HandlerContext, source: Row, reason: string, period?: { start: string; end: string }): Promise<void> {
285
+ const id = newId(ctx, ORDER);
286
+ const number = Number(ctx.mint('_seq_order'));
287
+ await ctx.record('_seq_order', {}, String(number));
288
+ const product = rowOf(ctx, PRODUCT, String(source.product_id))!;
289
+ const prices = (source.prices && !Array.isArray(source.prices) ? (source.prices as Record<string, Row[]>)[String(product.id)] : source.prices) as Row[] | undefined;
290
+ const items: Row[] = [];
291
+ for (const price of prices ?? product.prices as Row[]) {
292
+ let amount = price.amount_type === 'fixed' ? Number(price.price_amount) : price.amount_type === 'custom' ? Number(source.amount ?? 0) : 0;
293
+ // source: spec:/components/schemas/ProductPriceMeteredUnit/properties/unit_amount "The price per unit in cents."
294
+ // source: https://polar.sh/docs/features/usage-based-billing/event-ingestion "Polar never issues retroactive invoices or credits for late events."
295
+ if (price.amount_type === 'metered_unit' && period) {
296
+ const meter = rowOf(ctx, METER, String(price.meter_id))!;
297
+ const events = rowsOf(ctx, EVENT).filter((e) => e.customer_id === source.customer_id && String(e._received_at ?? e.timestamp) >= period.start && String(e._received_at ?? e.timestamp) < period.end && admits(meter.filter as Row, e));
298
+ amount = Math.ceil(aggregate(meter.aggregation as Row, events) * Number(price.unit_amount));
299
+ if (price.cap_amount != null) amount = Math.min(amount, Number(price.cap_amount));
300
+ }
301
+ const itemId = ctx.mint('_order_item');
302
+ await ctx.record('_order_item', {}, itemId);
303
+ items.push({ id: itemId, created_at: ctx.occurredAt, modified_at: null, label: product.name, amount, tax_amount: 0, proration: false, product_price_id: price.id });
304
+ }
305
+ const total = items.reduce((n, item) => n + Number(item.amount), 0);
306
+ const fields = orderFields(ctx, {
307
+ id, created_at: ctx.occurredAt, modified_at: null, status: 'pending', total_amount: total, currency: source.currency ?? 'usd', refunded_amount: 0,
308
+ billing_reason: reason, customer_id: source.customer_id, product_id: source.product_id, subscription_id: source.subscription_id ?? null,
309
+ checkout_id: source.checkout_id ?? source.id, metadata: source.metadata ?? {}, number, items,
310
+ });
311
+ await ctx.write(ORDER, id, { ...fields, _environment: environment(ctx) }, 'order.create');
312
+ await ctx.write(ORDER, id, { status: 'paid', paid: true, modified_at: ctx.occurredAt }, 'order.pay');
313
+ }
314
+
315
+ // source: https://polar.sh/docs/features/subscriptions/introduction "Daily, weekly, monthly or yearly."
316
+ export function addInterval(at: string, interval: string, count: number): string {
317
+ const d = new Date(at);
318
+ if (interval === 'day' || interval === 'week') d.setUTCDate(d.getUTCDate() + count * (interval === 'week' ? 7 : 1));
319
+ else {
320
+ const day = d.getUTCDate();
321
+ d.setUTCDate(1);
322
+ if (interval === 'year') d.setUTCFullYear(d.getUTCFullYear() + count);
323
+ else d.setUTCMonth(d.getUTCMonth() + count);
324
+ const last = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 0)).getUTCDate();
325
+ d.setUTCDate(Math.min(day, last));
326
+ }
327
+ return d.toISOString();
328
+ }
329
+
330
+ // source: https://polar.sh/docs/features/usage-based-billing/meters "Operators are the operators that you want to use to filter the events."
331
+ function compareClause(operator: string, actual: unknown, want: unknown): boolean {
332
+ switch (operator) {
333
+ case 'ne': return String(actual) !== String(want);
334
+ case 'gt': return Number(actual) > Number(want);
335
+ case 'gte': return Number(actual) >= Number(want);
336
+ case 'lt': return Number(actual) < Number(want);
337
+ case 'lte': return Number(actual) <= Number(want);
338
+ case 'like': return String(actual ?? '').includes(String(want));
339
+ case 'not_like': return !String(actual ?? '').includes(String(want));
340
+ default: return false;
341
+ }
342
+ }
343
+
344
+ // source: spec:/components/schemas/CustomerSession "CustomerSession"
345
+ export async function createSession(ctx: HandlerContext, customer: Row, returnUrl: unknown = null): Promise<Row> {
346
+ const id = newId(ctx, SESSION);
347
+ const token = await secret(ctx, 'polar_cst_', `session:${id}`);
348
+ const fields = { id, created_at: ctx.occurredAt, modified_at: null, token, expires_at: new Date(Date.parse(ctx.occurredAt) + 3_600_000).toISOString(), return_url: returnUrl,
349
+ customer_portal_url: `${PORTAL_ORIGIN}/${String(organizationView(ctx).slug)}/portal?customer_session_token=${token}`, customer_id: customer.id, customer: customerView(ctx, customer) };
350
+ await ctx.write(SESSION, id, { ...fields, _environment: environment(ctx) }, 'customer_session.create');
351
+ return fields;
352
+ }
@@ -0,0 +1,50 @@
1
+ // Polar's state machines, each move cited. Payment failures, refunds and customer cancellation are gaps.
2
+ // cancel_at_period_end stays false: neither the merchant mutation nor portal cancellation is served.
3
+ import type { StateField } from '@volter/world-core';
4
+
5
+ /** A checkout's status: open until the customer confirms it on the hosted page (an outside party), then Polar takes the
6
+ * payment; it expires 24 hours after it was made. */
7
+ // source: spec:/components/schemas/CheckoutStatus "succeeded"
8
+ const checkoutStatus: StateField = {
9
+ initial: 'open',
10
+ transitions: [
11
+ {
12
+ operation: 'checkouts:client_confirm', actor: 'external', from: ['open'], to: 'confirmed',
13
+ source: 'spec:checkouts:client_confirm "The checkout is expired, the customer already has an active subscription, or the organization is not ready to accept payments."',
14
+ },
15
+ { operation: 'checkouts:client_confirm', actor: 'external', from: ['confirmed', 'succeeded', 'failed', 'expired'], refusal: { status: 403, code: 'CheckoutForbiddenError', message: 'The checkout is expired, the customer already has an active subscription, or the organization is not ready to accept payments.' }, source: 'spec:checkouts:client_confirm "The checkout is expired, the customer already has an active subscription, or the organization is not ready to accept payments."' },
16
+ { actor: 'vendor', from: ['confirmed'], to: 'succeeded', source: 'spec:checkouts:client_confirm "Orders and subscriptions will be processed."' },
17
+ { actor: 'time', from: ['open'], to: 'expired', source: 'spec:checkouts:client_confirm "The checkout session is expired."' },
18
+ ],
19
+ };
20
+
21
+ /** A subscription's status: it starts incomplete until its first payment (active, or trialing through a trial), and is
22
+ * canceled when its period ends after a cancellation. No operation the twin serves moves it further: the
23
+ * subscriptions' API (update, revoke) is the gap, no application of the demand calling it. */
24
+ // source: spec:/components/schemas/SubscriptionStatus "incomplete"
25
+ const subscriptionStatus: StateField = {
26
+ initial: 'incomplete',
27
+ transitions: [
28
+ { operation: 'checkouts:client_confirm', actor: 'external', from: ['active', 'trialing'], refusal: { status: 403, code: 'CheckoutForbiddenError', message: 'The customer already has an active subscription.' }, source: 'spec:checkouts:client_confirm "the customer already has an active subscription"' },
29
+ { actor: 'vendor', from: ['incomplete'], to: 'active', source: 'spec:/components/schemas/SubscriptionStatus "active"' },
30
+ { actor: 'vendor', from: ['incomplete'], to: 'trialing', source: 'spec:/components/schemas/SubscriptionStatus "trialing"' },
31
+ { actor: 'time', from: ['trialing'], to: 'active', source: 'https://polar.sh/docs/features/subscriptions/trials "Once the trial period ends, the customer will be automatically charged for the subscription, and their billing cycle will begin."' },
32
+ ],
33
+ };
34
+
35
+ /** An order's status: pending until paid; a refund moves a paid order. */
36
+ // source: spec:/components/schemas/OrderStatus "partially_refunded"
37
+ const orderStatus: StateField = {
38
+ initial: 'pending',
39
+ transitions: [
40
+ { actor: 'vendor', from: ['pending'], to: 'paid', source: 'spec:/components/schemas/OrderStatus "paid"' },
41
+ ],
42
+ };
43
+
44
+ export const states = {
45
+ Checkout: { state: { status: checkoutStatus }, notState: ['is_business_customer', 'is_discount_applicable', 'is_free_product_price', 'is_payment_form_required', 'is_payment_required', 'is_payment_setup_required'] },
46
+ Subscription: { state: { status: subscriptionStatus }, notState: ['recurring_interval', 'cancel_at_period_end'] },
47
+ Order: { state: { status: orderStatus }, notState: ['billing_reason', 'is_invoice_generated', 'paid'] },
48
+ Meter: { notState: ['unit'] },
49
+ WebhookEndpoint: { notState: ['format'] },
50
+ };