@volter/twin-polar 2.0.37 → 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.
- package/README.md +24 -27
- package/dist/generated/pack-facts.json +69 -0
- package/dist/journeys/coverage.json +223 -0
- package/dist/journeys/customer-life.json +1222 -0
- package/dist/journeys/customer-life.outline.md +69 -0
- package/dist/journeys/customer-life.outline.verdict.json +41 -0
- package/dist/journeys/customer-life.verdict.json +71 -0
- package/dist/journeys/decisions.json +149 -0
- package/dist/journeys/demand.json +45 -0
- package/dist/journeys/score.json +249 -0
- package/dist/journeys/unreachable.json +13 -0
- package/dist/journeys/vendor-examples.json +9621 -0
- package/dist/journeys/vendor-examples.verdict.json +33 -0
- package/dist/spec/SOURCE.md +25 -0
- package/dist/spec/doc-examples.json +681 -0
- package/dist/spec/openapi.json.gz +0 -0
- package/dist/spec/sources.json +71 -0
- package/dist/src/cli.js +7 -16
- package/dist/src/fetch.d.ts +2 -0
- package/dist/src/fetch.js +24 -0
- package/dist/src/generated/surface.gen.json +1 -0
- package/dist/src/generated/ui.gen.json +1 -0
- package/dist/src/index.d.ts +5 -9
- package/dist/src/index.js +10 -55
- package/dist/src/manifest.d.ts +2 -0
- package/dist/src/manifest.js +123 -0
- package/dist/src/screens/checkout.d.ts +2 -0
- package/dist/src/screens/checkout.js +25 -0
- package/dist/src/screens/portal.d.ts +2 -0
- package/dist/src/screens/portal.js +18 -0
- package/dist/src/semantics/around.d.ts +2 -0
- package/dist/src/semantics/around.js +25 -0
- package/dist/src/semantics/checkouts.d.ts +14 -0
- package/dist/src/semantics/checkouts.js +131 -0
- package/dist/src/semantics/clock.d.ts +2 -0
- package/dist/src/semantics/clock.js +22 -0
- package/dist/src/semantics/customer-sessions.d.ts +2 -0
- package/dist/src/semantics/customer-sessions.js +12 -0
- package/dist/src/semantics/customers.d.ts +9 -0
- package/dist/src/semantics/customers.js +61 -0
- package/dist/src/semantics/doors.d.ts +11 -0
- package/dist/src/semantics/doors.js +47 -0
- package/dist/src/semantics/events.d.ts +12 -0
- package/dist/src/semantics/events.js +60 -0
- package/dist/src/semantics/index.d.ts +2 -0
- package/dist/src/semantics/index.js +27 -0
- package/dist/src/semantics/orders.d.ts +4 -0
- package/dist/src/semantics/orders.js +11 -0
- package/dist/src/semantics/products.d.ts +5 -0
- package/dist/src/semantics/products.js +43 -0
- package/dist/src/semantics/shared.d.ts +82 -0
- package/dist/src/semantics/shared.js +327 -0
- package/dist/src/semantics/states.d.ts +27 -0
- package/dist/src/semantics/states.js +43 -0
- package/dist/src/semantics/subscriptions.d.ts +4 -0
- package/dist/src/semantics/subscriptions.js +15 -0
- package/dist/src/semantics/tenant.d.ts +2 -0
- package/dist/src/semantics/tenant.js +2 -0
- package/dist/src/semantics/webhooks.d.ts +4 -0
- package/dist/src/semantics/webhooks.js +30 -0
- package/dist/src/server.d.ts +7 -0
- package/dist/src/server.js +7 -0
- package/generated/pack-facts.json +69 -0
- package/journeys/coverage.json +223 -0
- package/journeys/customer-life.json +1222 -0
- package/journeys/customer-life.outline.md +69 -0
- package/journeys/customer-life.outline.verdict.json +41 -0
- package/journeys/customer-life.verdict.json +71 -0
- package/journeys/decisions.json +149 -0
- package/journeys/demand.json +45 -0
- package/journeys/score.json +249 -0
- package/journeys/unreachable.json +13 -0
- package/journeys/vendor-examples.json +9621 -0
- package/journeys/vendor-examples.verdict.json +33 -0
- package/package.json +19 -31
- package/spec/SOURCE.md +25 -0
- package/spec/doc-examples.json +681 -0
- package/spec/openapi.json.gz +0 -0
- package/spec/sources.json +71 -0
- package/src/cli.ts +7 -14
- package/src/fetch.ts +25 -0
- package/src/generated/surface.gen.json +1 -0
- package/src/generated/ui.gen.json +1 -0
- package/src/index.ts +11 -85
- package/src/manifest.ts +135 -0
- package/src/screens/checkout.ts +33 -0
- package/src/screens/portal.ts +20 -0
- package/src/semantics/around.ts +25 -0
- package/src/semantics/checkouts.ts +133 -0
- package/src/semantics/clock.ts +24 -0
- package/src/semantics/customer-sessions.ts +13 -0
- package/src/semantics/customers.ts +68 -0
- package/src/semantics/doors.ts +51 -0
- package/src/semantics/events.ts +62 -0
- package/src/semantics/index.ts +31 -0
- package/src/semantics/orders.ts +15 -0
- package/src/semantics/products.ts +45 -0
- package/src/semantics/shared.ts +352 -0
- package/src/semantics/states.ts +50 -0
- package/src/semantics/subscriptions.ts +18 -0
- package/src/semantics/tenant.ts +5 -0
- package/src/semantics/webhooks.ts +29 -0
- package/src/server.ts +8 -0
- package/dist/src/polar-budget.d.ts +0 -83
- package/dist/src/polar-budget.js +0 -413
- package/dist/src/polar-capabilities.d.ts +0 -4
- package/dist/src/polar-capabilities.js +0 -389
- package/dist/src/polar-conformance.d.ts +0 -7
- package/dist/src/polar-conformance.js +0 -25
- package/dist/src/polar-connector.d.ts +0 -113
- package/dist/src/polar-connector.js +0 -146
- package/dist/src/polar-server.d.ts +0 -14
- package/dist/src/polar-server.js +0 -52
- package/dist/src/polar-twin.d.ts +0 -24
- package/dist/src/polar-twin.js +0 -941
- package/src/polar-budget.ts +0 -459
- package/src/polar-capabilities.ts +0 -448
- package/src/polar-conformance.ts +0 -33
- package/src/polar-connector.ts +0 -172
- package/src/polar-server.ts +0 -57
- package/src/polar-twin.ts +0 -938
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/** A customer by its id or its external id. */
|
|
2
|
+
export function customerBy(ctx, key) {
|
|
3
|
+
return key.id !== undefined ? rowsOf(ctx, CUSTOMER).find((c) => c.id === key.id) : rowsOf(ctx, CUSTOMER).find((c) => c.external_id === key.external);
|
|
4
|
+
}
|
|
5
|
+
export const ORGANIZATION = 'organization';
|
|
6
|
+
export const CUSTOMER = 'customer';
|
|
7
|
+
export const PRODUCT = 'product';
|
|
8
|
+
export const CHECKOUT = 'checkout';
|
|
9
|
+
export const SUBSCRIPTION = 'subscription';
|
|
10
|
+
export const ORDER = 'order';
|
|
11
|
+
export const EVENT = 'event';
|
|
12
|
+
export const METER = 'meter';
|
|
13
|
+
export const SESSION = 'customer_session';
|
|
14
|
+
export const ENDPOINT = 'webhook_endpoint';
|
|
15
|
+
/** Where Polar's hosted pages are: the checkout (`buy.polar.sh`) and the portal (`polar.sh`), rewritten to the twin. */
|
|
16
|
+
export const CHECKOUT_ORIGIN = 'https://buy.polar.sh';
|
|
17
|
+
export const PORTAL_ORIGIN = 'https://polar.sh';
|
|
18
|
+
// ── answers ────────────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
// source: spec:/components/schemas/ResourceNotFound "ResourceNotFound"
|
|
20
|
+
/** Polar's error body: `{ error: <kind>, detail }`. */
|
|
21
|
+
export const fail = (status, error, detail) => Response.json({ error, detail }, { status });
|
|
22
|
+
export const notFound = (what) => fail(404, 'ResourceNotFound', `${what} not found.`);
|
|
23
|
+
/** FastAPI's 422, one error per field. */
|
|
24
|
+
// source: spec:/components/schemas/HTTPValidationError "HTTPValidationError"
|
|
25
|
+
export const invalid = (loc, msg, type = 'value_error') => Response.json({ detail: [{ type, loc: ['body', ...loc], msg, input: null }] }, { status: 422 });
|
|
26
|
+
export const missing = (field) => invalid([field], 'Field required', 'missing');
|
|
27
|
+
export const bodyOf = (ctx) => (ctx.body && typeof ctx.body === 'object' && !Array.isArray(ctx.body) ? ctx.body : {});
|
|
28
|
+
// ── ids and the organization ───────────────────────────────────────────────────────────────────
|
|
29
|
+
/** A new subject's id: a UUID the kernel mints from the tree (ctx.mint), under the resource the manifest declares for its
|
|
30
|
+
* stored type. */
|
|
31
|
+
const DECLARED = { checkout: 'Checkout', subscription: 'Subscription', order: 'Order', webhook_endpoint: 'WebhookEndpoint' };
|
|
32
|
+
export const newId = (ctx, type) => ctx.mint(DECLARED[type] ?? type);
|
|
33
|
+
/** A secret of Polar's form (`polar_cst_…`, `polar_c_…`, `polar_whs_…`, `polar_oat_…`): its prefix and 43 letters and
|
|
34
|
+
* digits drawn from a secret the World holds for the seed (ctx.secret), so none is computed from the ids it names. */
|
|
35
|
+
export async function secret(ctx, prefix, seed) {
|
|
36
|
+
const a = await ctx.secret(`polar-secret:${seed}`);
|
|
37
|
+
const b = await ctx.secret(`polar-secret:${seed}:2`);
|
|
38
|
+
return `${prefix}${`${a}${b}`.replace(/[^A-Za-z0-9]/g, '').slice(0, 43)}`;
|
|
39
|
+
}
|
|
40
|
+
/** The World's one organization: the one its token names. */
|
|
41
|
+
export const ORG_ID = '1dbfc517-0bbf-4301-9ba8-555ca42b9737';
|
|
42
|
+
export const orgId = (ctx) => environment(ctx) === 'sandbox' ? '2dbfc517-0bbf-4301-9ba8-555ca42b9737' : ORG_ID;
|
|
43
|
+
export const organizationView = (ctx) => {
|
|
44
|
+
const o = rowOf(ctx, ORGANIZATION, 'org') ?? {};
|
|
45
|
+
return {
|
|
46
|
+
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',
|
|
47
|
+
avatar_url: null, proration_behavior: 'prorate', allow_customer_updates: true, email: null, website: null, socials: [], status: 'active',
|
|
48
|
+
details_submitted_at: null, onboarding_resubmission_requested_at: null, sso_enforced: false, default_presentment_currency: 'usd',
|
|
49
|
+
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 },
|
|
50
|
+
customer_email_settings: {}, customer_portal_settings: {}, dispute_settings: {}, embed_hosts: [], embed_hosts_enforced: false, account_id: null, payout_account_id: null, capabilities: {},
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
// ── views: each object with every field its schema requires ───────────────────────────────────
|
|
54
|
+
/** Whether a row's field is among the values a list's query names (one value or several), or the query names none. */
|
|
55
|
+
export const among = (ctx, key, value) => ctx.params[key] === undefined || [].concat(ctx.params[key]).map(String).includes(String(value));
|
|
56
|
+
/** Newest first by a timestamp field. */
|
|
57
|
+
export const newest = (field) => (a, b) => String(b[field]).localeCompare(String(a[field]));
|
|
58
|
+
/** Stored rows without the kernel's bookkeeping. */
|
|
59
|
+
export const environment = (ctx) => new URL(ctx.call.request.url).hostname.startsWith('sandbox') ? 'sandbox' : 'production';
|
|
60
|
+
export const rowsOf = (ctx, type) => ctx.rowsRaw(type).filter((r) => r.deleted !== true && (r._environment ?? (r.organization_id === '2dbfc517-0bbf-4301-9ba8-555ca42b9737' ? 'sandbox' : 'production')) === environment(ctx));
|
|
61
|
+
export const rowOf = (ctx, type, id) => rowsOf(ctx, type).find((r) => r.id === id);
|
|
62
|
+
/** The stored vendor shape; bookkeeping is private, including the environment boundary. */
|
|
63
|
+
export const vendorFields = (ctx, row) => Object.fromEntries(Object.entries(ctx.own(row)).filter(([k]) => !k.startsWith('_')));
|
|
64
|
+
export const customerView = (ctx, c) => vendorFields(ctx, c);
|
|
65
|
+
export const productView = (ctx, p) => vendorFields(ctx, p);
|
|
66
|
+
export const subscriptionView = (ctx, s) => vendorFields(ctx, s);
|
|
67
|
+
export const orderView = (ctx, o) => vendorFields(ctx, o);
|
|
68
|
+
export const eventView = (ctx, e) => vendorFields(ctx, e);
|
|
69
|
+
export const checkoutView = (ctx, c) => vendorFields(ctx, c);
|
|
70
|
+
// source: spec:/components/schemas/CustomerIndividual "CustomerIndividual"
|
|
71
|
+
export function customerFields(ctx, c) {
|
|
72
|
+
return {
|
|
73
|
+
id: c.id, created_at: c.created_at, modified_at: c.modified_at ?? null, metadata: c.metadata ?? {}, external_id: c.external_id ?? null,
|
|
74
|
+
email: c.email, email_verified: c.email_verified ?? false, type: 'individual', name: c.name ?? null, billing_name: c.billing_name ?? null,
|
|
75
|
+
billing_address: c.billing_address ?? null, tax_id: c.tax_id ?? null, locale: c.locale ?? null, organization_id: orgId(ctx),
|
|
76
|
+
default_payment_method_id: null, deleted_at: c.deleted_at ?? null, first_user_event_at: c.first_user_event_at ?? null,
|
|
77
|
+
avatar_url: `https://www.gravatar.com/avatar/${String(c.email ?? '')}?d=404`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** A price as the schema of its amount type requires (ProductPriceFixed, ProductPriceFree, ProductPriceCustom,
|
|
81
|
+
* ProductPriceMeteredUnit). */
|
|
82
|
+
// source: spec:/components/schemas/ProductPriceFixed "ProductPriceFixed"
|
|
83
|
+
export function priceView(ctx, p, product) {
|
|
84
|
+
const base = {
|
|
85
|
+
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',
|
|
86
|
+
tax_behavior: null, is_archived: p.is_archived ?? false, product_id: product.id,
|
|
87
|
+
};
|
|
88
|
+
if (p.amount_type === 'fixed')
|
|
89
|
+
return { ...base, price_amount: p.price_amount };
|
|
90
|
+
if (p.amount_type === 'custom')
|
|
91
|
+
return { ...base, minimum_amount: p.minimum_amount ?? null, maximum_amount: p.maximum_amount ?? null, preset_amount: p.preset_amount ?? null };
|
|
92
|
+
if (p.amount_type !== 'metered_unit')
|
|
93
|
+
return base;
|
|
94
|
+
const meter = rowOf(ctx, METER, String(p.meter_id));
|
|
95
|
+
// source: spec:/components/schemas/ProductPriceMeter "A meter associated to a metered price."
|
|
96
|
+
// source: spec:/components/schemas/MeterCreate/properties/unit "scalar"
|
|
97
|
+
return { ...base, unit_amount: String(p.unit_amount ?? '0'), cap_amount: p.cap_amount ?? null, meter_id: p.meter_id,
|
|
98
|
+
meter: { id: p.meter_id, name: meter?.name ?? '', unit: meter?.unit ?? 'scalar', custom_label: meter?.custom_label ?? null, custom_multiplier: meter?.custom_multiplier ?? null } };
|
|
99
|
+
}
|
|
100
|
+
// source: spec:/components/schemas/Product "Product"
|
|
101
|
+
export function productFields(ctx, p) {
|
|
102
|
+
return {
|
|
103
|
+
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,
|
|
104
|
+
name: p.name, description: p.description ?? null, visibility: p.visibility ?? 'public', recurring_interval: p.recurring_interval ?? null,
|
|
105
|
+
recurring_interval_count: p.recurring_interval ? Number(p.recurring_interval_count ?? 1) : null, meter_interval: null, meter_interval_count: null,
|
|
106
|
+
is_recurring: p.recurring_interval != null, is_archived: p.is_archived ?? false, organization_id: orgId(ctx), metadata: p.metadata ?? {},
|
|
107
|
+
prices: p.prices ?? [],
|
|
108
|
+
benefits: [],
|
|
109
|
+
medias: [], attached_custom_fields: [],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// source: spec:/components/schemas/Subscription "Subscription"
|
|
113
|
+
export function subscriptionFields(ctx, s) {
|
|
114
|
+
const customer = rowOf(ctx, CUSTOMER, String(s.customer_id));
|
|
115
|
+
const product = rowOf(ctx, PRODUCT, String(s.product_id));
|
|
116
|
+
return {
|
|
117
|
+
created_at: s.created_at, modified_at: s.modified_at ?? null, id: s.id, amount: s.amount ?? 0, currency: s.currency ?? 'usd',
|
|
118
|
+
recurring_interval: s.recurring_interval, recurring_interval_count: Number(s.recurring_interval_count ?? 1), status: s.status,
|
|
119
|
+
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,
|
|
120
|
+
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,
|
|
121
|
+
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,
|
|
122
|
+
customer_id: s.customer_id, product_id: s.product_id, discount_id: s.discount_id ?? null, checkout_id: s.checkout_id ?? null,
|
|
123
|
+
customer_cancellation_reason: s.customer_cancellation_reason ?? null, customer_cancellation_comment: s.customer_cancellation_comment ?? null,
|
|
124
|
+
metadata: s.metadata ?? {}, custom_field_data: {},
|
|
125
|
+
customer: customer ? customerView(ctx, customer) : null, product: product ? productView(ctx, product) : null, discount: null,
|
|
126
|
+
prices: s.prices ?? product?.prices ?? [], meters: s.meters ?? [], pending_update: null,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// source: spec:/components/schemas/Order "Order"
|
|
130
|
+
export function orderFields(ctx, o) {
|
|
131
|
+
const customer = rowOf(ctx, CUSTOMER, String(o.customer_id));
|
|
132
|
+
const product = o.product_id ? rowOf(ctx, PRODUCT, String(o.product_id)) : undefined;
|
|
133
|
+
const sub = o.subscription_id ? rowOf(ctx, SUBSCRIPTION, String(o.subscription_id)) : undefined;
|
|
134
|
+
const total = Number(o.total_amount ?? 0);
|
|
135
|
+
const refunded = Number(o.refunded_amount ?? 0);
|
|
136
|
+
return {
|
|
137
|
+
id: o.id, created_at: o.created_at, modified_at: o.modified_at ?? null, status: o.status, paid: o.status !== 'pending',
|
|
138
|
+
subtotal_amount: total, discount_amount: 0, net_amount: total, tax_amount: 0, total_amount: total, applied_balance_amount: 0, due_amount: 0,
|
|
139
|
+
refunded_amount: refunded, refunded_tax_amount: 0, currency: o.currency ?? 'usd', billing_reason: o.billing_reason, billing_name: null, billing_address: null,
|
|
140
|
+
invoice_number: `TWIN-${String(o.number ?? 1).padStart(4, '0')}`, is_invoice_generated: false, receipt_number: null,
|
|
141
|
+
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,
|
|
142
|
+
metadata: o.metadata ?? {}, custom_field_data: {}, platform_fee_amount: 0, platform_fee_currency: null,
|
|
143
|
+
customer: customer ? customerView(ctx, customer) : null,
|
|
144
|
+
product: product ? (({ prices: _p, benefits: _b, medias: _m, attached_custom_fields: _a, ...rest }) => rest)(productView(ctx, product)) : null,
|
|
145
|
+
discount: null, subscription: sub ? (({ customer: _c, product: _pr, discount: _d, prices: _ps, meters: _ms, pending_update: _u, ...rest }) => rest)(subscriptionView(ctx, sub)) : null,
|
|
146
|
+
items: o.items ?? [],
|
|
147
|
+
description: product?.name ?? 'Order', refundable_amount: total - refunded, refundable_tax_amount: 0,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/** A usage event as the API answers it (UserEvent): with its customer. */
|
|
151
|
+
// source: spec:/components/schemas/UserEvent "An event you created through the ingestion API."
|
|
152
|
+
// Where the documentation stops: a user event's `label` (its type's "human readable label"); the twin gives its name
|
|
153
|
+
export function eventFields(ctx, e) {
|
|
154
|
+
const customer = e.customer_id ? rowOf(ctx, CUSTOMER, String(e.customer_id)) : undefined;
|
|
155
|
+
return {
|
|
156
|
+
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,
|
|
157
|
+
external_customer_id: e.external_customer_id ?? null, member_id: null, external_member_id: null, child_count: 0, parent_id: null,
|
|
158
|
+
label: e.name, name: e.name, source: 'user', metadata: e.metadata ?? {},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
// source: spec:/components/schemas/Checkout "Checkout"
|
|
162
|
+
export function checkoutFields(ctx, c) {
|
|
163
|
+
const product = rowOf(ctx, PRODUCT, String(c.product_id));
|
|
164
|
+
const pv = product ? productView(ctx, product) : null;
|
|
165
|
+
const prices = c.prices?.[String(c.product_id)] ?? pv?.prices ?? [];
|
|
166
|
+
const price = prices.find((p) => p.id === c.product_price_id) ?? prices[0] ?? null;
|
|
167
|
+
const amount = Number(c.amount ?? 0);
|
|
168
|
+
return {
|
|
169
|
+
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,
|
|
170
|
+
url: `${CHECKOUT_ORIGIN}/${String(c.client_secret)}`, expires_at: c.expires_at, success_url: c.success_url ?? `${CHECKOUT_ORIGIN}/${String(c.client_secret)}/confirmation`,
|
|
171
|
+
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,
|
|
172
|
+
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,
|
|
173
|
+
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,
|
|
174
|
+
require_billing_address: c.require_billing_address ?? false, is_discount_applicable: false, is_free_product_price: amount === 0, is_payment_required: amount > 0,
|
|
175
|
+
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,
|
|
176
|
+
customer_name: c.customer_name ?? null, customer_email: c.customer_email ?? null, customer_ip_address: c.customer_ip_address ?? null, customer_billing_name: null,
|
|
177
|
+
customer_billing_address: null, customer_tax_id: null, payment_method_type: null, payment_processor_metadata: {},
|
|
178
|
+
billing_address_fields: { country: 'required', state: 'disabled', city: 'disabled', postal_code: 'disabled', line1: 'disabled', line2: 'disabled' },
|
|
179
|
+
trial_interval: null, trial_interval_count: null, metadata: c.metadata ?? {}, external_customer_id: c.external_customer_id ?? null, custom_field_data: {},
|
|
180
|
+
products: pv ? [pv] : [], product: pv, product_price: price, prices: pv ? { [String(pv.id)]: prices } : {}, discount: null,
|
|
181
|
+
subscription_id: c.subscription_id ?? null, attached_custom_fields: [], customer_metadata: c.customer_metadata ?? {},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
// ── a customer's meters ────────────────────────────────────────────────────────────────────────
|
|
185
|
+
/** Whether a meter's filter admits an event: clauses on `name`, `source` or a metadata key, joined by the filter's
|
|
186
|
+
* conjunction, nested filters among them. */
|
|
187
|
+
// source: spec:/components/schemas/Filter "Filter"
|
|
188
|
+
export function admits(filter, e) {
|
|
189
|
+
const clauses = filter?.clauses ?? [];
|
|
190
|
+
if (clauses.length === 0)
|
|
191
|
+
return true;
|
|
192
|
+
const one = (c) => {
|
|
193
|
+
if ('clauses' in c)
|
|
194
|
+
return admits(c, e);
|
|
195
|
+
const prop = String(c.property ?? '');
|
|
196
|
+
const actual = prop === 'name' || prop === 'source' ? e[prop] : (e.metadata ?? {})[prop.replace(/^metadata\./, '')];
|
|
197
|
+
const want = c.value;
|
|
198
|
+
switch (String(c.operator ?? 'eq')) {
|
|
199
|
+
case 'eq': return String(actual) === String(want);
|
|
200
|
+
default: return compareClause(String(c.operator), actual, want);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
return String(filter?.conjunction ?? 'and') === 'or' ? clauses.some(one) : clauses.every(one);
|
|
204
|
+
}
|
|
205
|
+
/** A meter's aggregation over the events it admits: a count, or the sum, max, min or average of a metadata property. */
|
|
206
|
+
// source: spec:/components/schemas/PropertyAggregation "PropertyAggregation"
|
|
207
|
+
export function aggregate(agg, events) {
|
|
208
|
+
const func = String(agg?.func ?? 'count');
|
|
209
|
+
if (func === 'count')
|
|
210
|
+
return events.length;
|
|
211
|
+
const prop = String(agg?.property ?? '').replace(/^metadata\./, '');
|
|
212
|
+
// source: https://polar.sh/docs/features/usage-based-billing/meters "Counts the number of unique values of a property."
|
|
213
|
+
if (func === 'unique')
|
|
214
|
+
return new Set(events.map((e) => (e.metadata ?? {})[prop])).size;
|
|
215
|
+
const values = events.map((e) => Number((e.metadata ?? {})[prop])).filter(Number.isFinite);
|
|
216
|
+
if (values.length === 0)
|
|
217
|
+
return 0;
|
|
218
|
+
if (func === 'max')
|
|
219
|
+
return Math.max(...values);
|
|
220
|
+
if (func === 'min')
|
|
221
|
+
return Math.min(...values);
|
|
222
|
+
const sum = values.reduce((a, b) => a + b, 0);
|
|
223
|
+
return func === 'avg' ? sum / values.length : sum;
|
|
224
|
+
}
|
|
225
|
+
/** A customer's meter: consumed (the fold over its events), credited (its meter-credit grants), and the balance. */
|
|
226
|
+
// source: spec:/components/schemas/CustomerStateMeter "The balance of the meter, i.e. the difference between credited and consumed units."
|
|
227
|
+
export function customerMeter(ctx, customerId, meter) {
|
|
228
|
+
const events = rowsOf(ctx, EVENT).filter((e) => e.customer_id === customerId && admits(meter.filter, e));
|
|
229
|
+
const consumed = aggregate(meter.aggregation, events);
|
|
230
|
+
const credited = 0; // No demanded product grants meter credits; benefits are outside the served surface.
|
|
231
|
+
return { consumed, credited, balance: credited - consumed };
|
|
232
|
+
}
|
|
233
|
+
/** A customer's state: its active subscriptions, granted benefits and meters. */
|
|
234
|
+
// source: spec:customers:get_state "The customer state includes information about the customer's active subscriptions and benefits."
|
|
235
|
+
export async function customerState(ctx, c) {
|
|
236
|
+
const id = String(c.id);
|
|
237
|
+
for (const meter of rowsOf(ctx, METER)) {
|
|
238
|
+
if (!rowsOf(ctx, '_customer_meter').some((r) => r.customer_id === id && r.meter_id === meter.id)) {
|
|
239
|
+
const meterId = ctx.mint('_customer_meter');
|
|
240
|
+
await ctx.record('_customer_meter', { id: meterId, customer_id: id, meter_id: meter.id, _environment: environment(ctx) }, meterId);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const subs = rowsOf(ctx, SUBSCRIPTION).filter((s) => s.customer_id === id && (s.status === 'active' || s.status === 'trialing'));
|
|
244
|
+
return {
|
|
245
|
+
...customerView(ctx, c),
|
|
246
|
+
active_subscriptions: subs.map((s) => {
|
|
247
|
+
const v = subscriptionView(ctx, s);
|
|
248
|
+
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: {} };
|
|
249
|
+
}),
|
|
250
|
+
granted_benefits: [],
|
|
251
|
+
active_meters: rowsOf(ctx, METER).map((m) => {
|
|
252
|
+
const { consumed, credited, balance } = customerMeter(ctx, id, m);
|
|
253
|
+
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 };
|
|
254
|
+
}),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
// source: https://polar.sh/docs/features/subscriptions/introduction "creates a new order"
|
|
258
|
+
export async function makeOrder(ctx, source, reason, period) {
|
|
259
|
+
const id = newId(ctx, ORDER);
|
|
260
|
+
const number = Number(ctx.mint('_seq_order'));
|
|
261
|
+
await ctx.record('_seq_order', {}, String(number));
|
|
262
|
+
const product = rowOf(ctx, PRODUCT, String(source.product_id));
|
|
263
|
+
const prices = (source.prices && !Array.isArray(source.prices) ? source.prices[String(product.id)] : source.prices);
|
|
264
|
+
const items = [];
|
|
265
|
+
for (const price of prices ?? product.prices) {
|
|
266
|
+
let amount = price.amount_type === 'fixed' ? Number(price.price_amount) : price.amount_type === 'custom' ? Number(source.amount ?? 0) : 0;
|
|
267
|
+
// source: spec:/components/schemas/ProductPriceMeteredUnit/properties/unit_amount "The price per unit in cents."
|
|
268
|
+
// source: https://polar.sh/docs/features/usage-based-billing/event-ingestion "Polar never issues retroactive invoices or credits for late events."
|
|
269
|
+
if (price.amount_type === 'metered_unit' && period) {
|
|
270
|
+
const meter = rowOf(ctx, METER, String(price.meter_id));
|
|
271
|
+
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, e));
|
|
272
|
+
amount = Math.ceil(aggregate(meter.aggregation, events) * Number(price.unit_amount));
|
|
273
|
+
if (price.cap_amount != null)
|
|
274
|
+
amount = Math.min(amount, Number(price.cap_amount));
|
|
275
|
+
}
|
|
276
|
+
const itemId = ctx.mint('_order_item');
|
|
277
|
+
await ctx.record('_order_item', {}, itemId);
|
|
278
|
+
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 });
|
|
279
|
+
}
|
|
280
|
+
const total = items.reduce((n, item) => n + Number(item.amount), 0);
|
|
281
|
+
const fields = orderFields(ctx, {
|
|
282
|
+
id, created_at: ctx.occurredAt, modified_at: null, status: 'pending', total_amount: total, currency: source.currency ?? 'usd', refunded_amount: 0,
|
|
283
|
+
billing_reason: reason, customer_id: source.customer_id, product_id: source.product_id, subscription_id: source.subscription_id ?? null,
|
|
284
|
+
checkout_id: source.checkout_id ?? source.id, metadata: source.metadata ?? {}, number, items,
|
|
285
|
+
});
|
|
286
|
+
await ctx.write(ORDER, id, { ...fields, _environment: environment(ctx) }, 'order.create');
|
|
287
|
+
await ctx.write(ORDER, id, { status: 'paid', paid: true, modified_at: ctx.occurredAt }, 'order.pay');
|
|
288
|
+
}
|
|
289
|
+
// source: https://polar.sh/docs/features/subscriptions/introduction "Daily, weekly, monthly or yearly."
|
|
290
|
+
export function addInterval(at, interval, count) {
|
|
291
|
+
const d = new Date(at);
|
|
292
|
+
if (interval === 'day' || interval === 'week')
|
|
293
|
+
d.setUTCDate(d.getUTCDate() + count * (interval === 'week' ? 7 : 1));
|
|
294
|
+
else {
|
|
295
|
+
const day = d.getUTCDate();
|
|
296
|
+
d.setUTCDate(1);
|
|
297
|
+
if (interval === 'year')
|
|
298
|
+
d.setUTCFullYear(d.getUTCFullYear() + count);
|
|
299
|
+
else
|
|
300
|
+
d.setUTCMonth(d.getUTCMonth() + count);
|
|
301
|
+
const last = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 0)).getUTCDate();
|
|
302
|
+
d.setUTCDate(Math.min(day, last));
|
|
303
|
+
}
|
|
304
|
+
return d.toISOString();
|
|
305
|
+
}
|
|
306
|
+
// source: https://polar.sh/docs/features/usage-based-billing/meters "Operators are the operators that you want to use to filter the events."
|
|
307
|
+
function compareClause(operator, actual, want) {
|
|
308
|
+
switch (operator) {
|
|
309
|
+
case 'ne': return String(actual) !== String(want);
|
|
310
|
+
case 'gt': return Number(actual) > Number(want);
|
|
311
|
+
case 'gte': return Number(actual) >= Number(want);
|
|
312
|
+
case 'lt': return Number(actual) < Number(want);
|
|
313
|
+
case 'lte': return Number(actual) <= Number(want);
|
|
314
|
+
case 'like': return String(actual ?? '').includes(String(want));
|
|
315
|
+
case 'not_like': return !String(actual ?? '').includes(String(want));
|
|
316
|
+
default: return false;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// source: spec:/components/schemas/CustomerSession "CustomerSession"
|
|
320
|
+
export async function createSession(ctx, customer, returnUrl = null) {
|
|
321
|
+
const id = newId(ctx, SESSION);
|
|
322
|
+
const token = await secret(ctx, 'polar_cst_', `session:${id}`);
|
|
323
|
+
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,
|
|
324
|
+
customer_portal_url: `${PORTAL_ORIGIN}/${String(organizationView(ctx).slug)}/portal?customer_session_token=${token}`, customer_id: customer.id, customer: customerView(ctx, customer) };
|
|
325
|
+
await ctx.write(SESSION, id, { ...fields, _environment: environment(ctx) }, 'customer_session.create');
|
|
326
|
+
return fields;
|
|
327
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { StateField } from '@volter/world-core';
|
|
2
|
+
export declare const states: {
|
|
3
|
+
Checkout: {
|
|
4
|
+
state: {
|
|
5
|
+
status: StateField;
|
|
6
|
+
};
|
|
7
|
+
notState: string[];
|
|
8
|
+
};
|
|
9
|
+
Subscription: {
|
|
10
|
+
state: {
|
|
11
|
+
status: StateField;
|
|
12
|
+
};
|
|
13
|
+
notState: string[];
|
|
14
|
+
};
|
|
15
|
+
Order: {
|
|
16
|
+
state: {
|
|
17
|
+
status: StateField;
|
|
18
|
+
};
|
|
19
|
+
notState: string[];
|
|
20
|
+
};
|
|
21
|
+
Meter: {
|
|
22
|
+
notState: string[];
|
|
23
|
+
};
|
|
24
|
+
WebhookEndpoint: {
|
|
25
|
+
notState: string[];
|
|
26
|
+
};
|
|
27
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** A checkout's status: open until the customer confirms it on the hosted page (an outside party), then Polar takes the
|
|
2
|
+
* payment; it expires 24 hours after it was made. */
|
|
3
|
+
// source: spec:/components/schemas/CheckoutStatus "succeeded"
|
|
4
|
+
const checkoutStatus = {
|
|
5
|
+
initial: 'open',
|
|
6
|
+
transitions: [
|
|
7
|
+
{
|
|
8
|
+
operation: 'checkouts:client_confirm', actor: 'external', from: ['open'], to: 'confirmed',
|
|
9
|
+
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."',
|
|
10
|
+
},
|
|
11
|
+
{ 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."' },
|
|
12
|
+
{ actor: 'vendor', from: ['confirmed'], to: 'succeeded', source: 'spec:checkouts:client_confirm "Orders and subscriptions will be processed."' },
|
|
13
|
+
{ actor: 'time', from: ['open'], to: 'expired', source: 'spec:checkouts:client_confirm "The checkout session is expired."' },
|
|
14
|
+
],
|
|
15
|
+
};
|
|
16
|
+
/** A subscription's status: it starts incomplete until its first payment (active, or trialing through a trial), and is
|
|
17
|
+
* canceled when its period ends after a cancellation. No operation the twin serves moves it further: the
|
|
18
|
+
* subscriptions' API (update, revoke) is the gap, no application of the demand calling it. */
|
|
19
|
+
// source: spec:/components/schemas/SubscriptionStatus "incomplete"
|
|
20
|
+
const subscriptionStatus = {
|
|
21
|
+
initial: 'incomplete',
|
|
22
|
+
transitions: [
|
|
23
|
+
{ 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"' },
|
|
24
|
+
{ actor: 'vendor', from: ['incomplete'], to: 'active', source: 'spec:/components/schemas/SubscriptionStatus "active"' },
|
|
25
|
+
{ actor: 'vendor', from: ['incomplete'], to: 'trialing', source: 'spec:/components/schemas/SubscriptionStatus "trialing"' },
|
|
26
|
+
{ 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."' },
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
/** An order's status: pending until paid; a refund moves a paid order. */
|
|
30
|
+
// source: spec:/components/schemas/OrderStatus "partially_refunded"
|
|
31
|
+
const orderStatus = {
|
|
32
|
+
initial: 'pending',
|
|
33
|
+
transitions: [
|
|
34
|
+
{ actor: 'vendor', from: ['pending'], to: 'paid', source: 'spec:/components/schemas/OrderStatus "paid"' },
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
export const states = {
|
|
38
|
+
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'] },
|
|
39
|
+
Subscription: { state: { status: subscriptionStatus }, notState: ['recurring_interval', 'cancel_at_period_end'] },
|
|
40
|
+
Order: { state: { status: orderStatus }, notState: ['billing_reason', 'is_invoice_generated', 'paid'] },
|
|
41
|
+
Meter: { notState: ['unit'] },
|
|
42
|
+
WebhookEndpoint: { notState: ['format'] },
|
|
43
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { HandlerContext } from '@volter/world-core';
|
|
2
|
+
/** `GET /v1/subscriptions/`: the organization's subscriptions, the latest started first, by customer (Polar's id or the
|
|
3
|
+
* application's), product or status. */
|
|
4
|
+
export declare function subscriptions_list(ctx: HandlerContext): Promise<Response>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { among, customerBy, newest, rowsOf, SUBSCRIPTION, subscriptionView } from "./shared.js";
|
|
2
|
+
/** `GET /v1/subscriptions/`: the organization's subscriptions, the latest started first, by customer (Polar's id or the
|
|
3
|
+
* application's), product or status. */
|
|
4
|
+
// source: spec:subscriptions:list "List subscriptions."
|
|
5
|
+
export async function subscriptions_list(ctx) {
|
|
6
|
+
const external = ctx.params.external_customer_id === undefined ? undefined : [].concat(ctx.params.external_customer_id).map((x) => String(customerBy(ctx, { external: String(x) })?.id));
|
|
7
|
+
// source: spec:subscriptions:list "Filter by active or inactive subscription."
|
|
8
|
+
const active = ctx.params.active === undefined ? undefined : String(ctx.params.active) === 'true';
|
|
9
|
+
const rows = rowsOf(ctx, SUBSCRIPTION)
|
|
10
|
+
.filter((s) => among(ctx, 'customer_id', s.customer_id) && among(ctx, 'product_id', s.product_id) && among(ctx, 'status', s.status))
|
|
11
|
+
.filter((s) => external === undefined || external.includes(String(s.customer_id)))
|
|
12
|
+
.filter((s) => active === undefined || ['trialing', 'active', 'past_due'].includes(String(s.status)) === active)
|
|
13
|
+
.sort(newest('started_at'));
|
|
14
|
+
return ctx.list('Subscription', rows.map((s) => subscriptionView(ctx, s)));
|
|
15
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { HandlerContext } from '@volter/world-core';
|
|
2
|
+
/** `POST /v1/webhooks/endpoints`: an endpoint for the events named, its signing secret drawn from the World
|
|
3
|
+
* (`polar_whs_`, ctx.secret) and answered. */
|
|
4
|
+
export declare function webhooks_create_webhook_endpoint(ctx: HandlerContext): Promise<Response>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { bodyOf, environment, ENDPOINT, invalid, missing, newId, orgId, secret } from "./shared.js";
|
|
2
|
+
const FORMATS = ['raw', 'discord', 'slack'];
|
|
3
|
+
/** `POST /v1/webhooks/endpoints`: an endpoint for the events named, its signing secret drawn from the World
|
|
4
|
+
* (`polar_whs_`, ctx.secret) and answered. */
|
|
5
|
+
// source: spec:webhooks:create_webhook_endpoint "Create a webhook endpoint."
|
|
6
|
+
export async function webhooks_create_webhook_endpoint(ctx) {
|
|
7
|
+
const body = bodyOf(ctx);
|
|
8
|
+
// source: spec:/components/schemas/WebhookEndpointCreate "The URL where the webhook events will be sent."
|
|
9
|
+
if (typeof body.url !== 'string' || body.url === '')
|
|
10
|
+
return missing('url');
|
|
11
|
+
if (!/^https?:\/\/[^\s]+$/.test(body.url))
|
|
12
|
+
return invalid(['url'], 'Input should be a valid URL', 'url_parsing');
|
|
13
|
+
// source: spec:/components/schemas/WebhookFormat "WebhookFormat"
|
|
14
|
+
if (body.format === undefined)
|
|
15
|
+
return missing('format');
|
|
16
|
+
if (!FORMATS.includes(String(body.format)))
|
|
17
|
+
return invalid(['format'], "Input should be 'raw', 'discord' or 'slack'", 'enum');
|
|
18
|
+
// source: spec:/components/schemas/WebhookEndpointCreate "The events that will trigger the webhook."
|
|
19
|
+
if (!Array.isArray(body.events))
|
|
20
|
+
return missing('events');
|
|
21
|
+
const id = newId(ctx, ENDPOINT);
|
|
22
|
+
const signing = await secret(ctx, 'polar_whs_', `endpoint:${id}`);
|
|
23
|
+
const endpoint = {
|
|
24
|
+
created_at: ctx.occurredAt, modified_at: null, id, url: body.url, name: body.name ?? null, format: body.format, secret: signing,
|
|
25
|
+
organization_id: orgId(ctx), events: body.events, enabled: true,
|
|
26
|
+
};
|
|
27
|
+
await ctx.write(ENDPOINT, id, { ...endpoint, _environment: environment(ctx) }, 'webhook_endpoint.create');
|
|
28
|
+
// source: spec:/components/schemas/WebhookEndpoint "The secret used to sign the webhook events."
|
|
29
|
+
return Response.json(endpoint, { status: 201 });
|
|
30
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// FETCH-FIRST (runtime contract R12b): a fixed file, the kernel's serve seam around the pack's fetch (./fetch.ts).
|
|
2
|
+
import { serveHttp } from '@volter/world-core';
|
|
3
|
+
import { createPolarFetch } from "./fetch.js";
|
|
4
|
+
export async function createPolarTwinServer(options = {}) {
|
|
5
|
+
const server = await serveHttp({ port: options.port ?? 0, idleTimeout: 60, fetch: createPolarFetch(options) });
|
|
6
|
+
return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
|
|
7
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"//": "GENERATED by scripts/pack-facts.ts — current pack descriptors and explicit world-runtime server backings; no prior-artifact entries are retained.",
|
|
3
|
+
"packs": {
|
|
4
|
+
"polar": {
|
|
5
|
+
"adoption": {
|
|
6
|
+
"sdks": [
|
|
7
|
+
"@polar-sh/sdk"
|
|
8
|
+
],
|
|
9
|
+
"pypi": [
|
|
10
|
+
"polar-sdk"
|
|
11
|
+
],
|
|
12
|
+
"scopes": [
|
|
13
|
+
"@polar-sh/"
|
|
14
|
+
],
|
|
15
|
+
"envStems": [
|
|
16
|
+
"POLAR"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"hosts": [
|
|
20
|
+
{
|
|
21
|
+
"host": "api.polar.sh"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"host": "sandbox-api.polar.sh"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"host": "buy.polar.sh"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"host": "polar.sh",
|
|
31
|
+
"pathPattern": "^/[^/]+/portal"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"host": "sandbox.polar.sh"
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"archetype": "crud",
|
|
38
|
+
"protocol": {
|
|
39
|
+
"declared": "3",
|
|
40
|
+
"major": 3,
|
|
41
|
+
"standing": "current"
|
|
42
|
+
},
|
|
43
|
+
"transport": "rest",
|
|
44
|
+
"resources": [
|
|
45
|
+
"customer",
|
|
46
|
+
"product",
|
|
47
|
+
"checkout",
|
|
48
|
+
"subscription",
|
|
49
|
+
"order",
|
|
50
|
+
"event",
|
|
51
|
+
"meter",
|
|
52
|
+
"customer_session",
|
|
53
|
+
"webhook_endpoint"
|
|
54
|
+
],
|
|
55
|
+
"endpointEnvNone": "@polar-sh/sdk reads no base-URL variable: its `server` option picks production or sandbox, both claimed by the host rules.",
|
|
56
|
+
"credentialDoor": {
|
|
57
|
+
"path": "/_twin/app-credentials",
|
|
58
|
+
"body": {},
|
|
59
|
+
"fill": {
|
|
60
|
+
"POLAR_ACCESS_TOKEN": "access_token",
|
|
61
|
+
"POLAR_API_KEY": "access_token",
|
|
62
|
+
"POLAR_ORGANIZATION_TOKEN": "access_token"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"serveExport": "createPolarTwinServer"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"undeclared": []
|
|
69
|
+
}
|