sparta_worlds 0.6.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 (93) hide show
  1. package/LICENSE.txt +15 -0
  2. package/bin/worlds.js +53 -0
  3. package/dist/cli/argv.js +27 -0
  4. package/dist/cli/claims.js +250 -0
  5. package/dist/cli/commands.js +67 -0
  6. package/dist/cli/contract.js +142 -0
  7. package/dist/cli/demo.js +114 -0
  8. package/dist/cli/doctor.js +299 -0
  9. package/dist/cli/expect.js +197 -0
  10. package/dist/cli/home.js +18 -0
  11. package/dist/cli/import.js +530 -0
  12. package/dist/cli/index.js +1014 -0
  13. package/dist/cli/init.js +725 -0
  14. package/dist/cli/judge-command.js +184 -0
  15. package/dist/cli/judge.js +1495 -0
  16. package/dist/cli/load.js +8 -0
  17. package/dist/cli/packs.js +82 -0
  18. package/dist/cli/passk.js +1256 -0
  19. package/dist/cli/policy-starters.js +83 -0
  20. package/dist/cli/preload.js +45 -0
  21. package/dist/cli/report.js +579 -0
  22. package/dist/cli/rubric.js +727 -0
  23. package/dist/cli/run.js +266 -0
  24. package/dist/cli/scoreboard.js +432 -0
  25. package/dist/cli/selftest.js +157 -0
  26. package/dist/cli/stdio.js +25 -0
  27. package/dist/cli/tasks.js +1246 -0
  28. package/dist/cli/try.js +156 -0
  29. package/dist/cli/version.js +17 -0
  30. package/package.json +38 -0
  31. package/packs/cancellations.yaml +201 -0
  32. package/packs/credits.yaml +274 -0
  33. package/packs/invoices.yaml +206 -0
  34. package/packs/plan-changes.yaml +170 -0
  35. package/packs/refunds.yaml +437 -0
  36. package/preload/node-hooks.mjs +28 -0
  37. package/preload/node.mjs +19 -0
  38. package/preload/python/sitecustomize.py +88 -0
  39. package/preload/wrap.mjs +81 -0
  40. package/rubrics/acme-prod-library.json +1427 -0
  41. package/rubrics/acme-prod-shift.json +499 -0
  42. package/server/dist/admin/auth.js +145 -0
  43. package/server/dist/admin/routes.js +469 -0
  44. package/server/dist/admin/schemas.js +243 -0
  45. package/server/dist/app.js +112 -0
  46. package/server/dist/core/canonical.js +22 -0
  47. package/server/dist/core/db.js +294 -0
  48. package/server/dist/core/errors.js +136 -0
  49. package/server/dist/core/manager.js +337 -0
  50. package/server/dist/core/money.js +43 -0
  51. package/server/dist/core/nondeterministic.js +39 -0
  52. package/server/dist/core/paths.js +33 -0
  53. package/server/dist/core/prng.js +19 -0
  54. package/server/dist/core/protocol.js +1 -0
  55. package/server/dist/core/publicUrl.js +49 -0
  56. package/server/dist/core/seed.js +286 -0
  57. package/server/dist/core/seedExport.js +169 -0
  58. package/server/dist/core/seedLoader.js +156 -0
  59. package/server/dist/core/time.js +45 -0
  60. package/server/dist/core/world.js +361 -0
  61. package/server/dist/index.js +17 -0
  62. package/server/dist/main.js +42 -0
  63. package/server/dist/scenario/engine.js +172 -0
  64. package/server/dist/scenario/library.js +49 -0
  65. package/server/dist/twin/build.js +372 -0
  66. package/server/dist/twin/pagination.js +32 -0
  67. package/server/dist/twin/params.js +118 -0
  68. package/server/dist/twin/resources/charges.js +24 -0
  69. package/server/dist/twin/resources/customers.js +151 -0
  70. package/server/dist/twin/resources/events.js +26 -0
  71. package/server/dist/twin/resources/invoices.js +217 -0
  72. package/server/dist/twin/resources/payment_intents.js +146 -0
  73. package/server/dist/twin/resources/products.js +92 -0
  74. package/server/dist/twin/resources/refunds.js +103 -0
  75. package/server/dist/twin/resources/shared.js +113 -0
  76. package/server/dist/twin/resources/subscriptions.js +325 -0
  77. package/server/dist/twin/resources/tickets.js +128 -0
  78. package/server/dist/twin/router.js +279 -0
  79. package/server/dist/twin/unsupported.js +212 -0
  80. package/server/dist/webhooks/dispatcher.js +148 -0
  81. package/server/scenarios/auth-expiry.yaml +7 -0
  82. package/server/scenarios/intermittent-500s.yaml +9 -0
  83. package/server/scenarios/lock-timeouts.yaml +12 -0
  84. package/server/scenarios/rate-limit-storm.yaml +10 -0
  85. package/server/scenarios/slow-stripe.yaml +9 -0
  86. package/server/scenarios/stripe-storm-shift.yaml +15 -0
  87. package/server/scenarios/tickets-storm.yaml +10 -0
  88. package/server/scenarios/webhook-chaos.yaml +7 -0
  89. package/server/seeds/acme-prod-library.json +40470 -0
  90. package/server/seeds/acme-prod-shift.json +36544 -0
  91. package/server/seeds/acme-prod.json +35412 -0
  92. package/server/seeds/saas-billing-small.json +4792 -0
  93. package/server/seeds/saas-billing-smoke.json +160 -0
@@ -0,0 +1,372 @@
1
+ import { createHash } from "node:crypto";
2
+ export const ACCOUNT_DEFAULT_CURRENCY = "usd";
3
+ export function invoicePrefixFor(customerId) {
4
+ const digest = createHash("sha256").update(`invoice_prefix:${customerId}`).digest();
5
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
6
+ let out = "";
7
+ for (let i = 0; i < 8; i++)
8
+ out += alphabet[digest[i] % alphabet.length];
9
+ return out;
10
+ }
11
+ export function customerShape(f) {
12
+ return {
13
+ id: f.id,
14
+ object: "customer",
15
+ balance: f.balance ?? 0,
16
+ created: f.created,
17
+ currency: f.currency ?? null,
18
+ default_source: null,
19
+ delinquent: false,
20
+ description: f.description ?? null,
21
+ email: f.email ?? null,
22
+ invoice_prefix: invoicePrefixFor(f.id),
23
+ invoice_settings: { custom_fields: null, default_payment_method: null, footer: null, rendering_options: null },
24
+ livemode: false,
25
+ metadata: f.metadata ?? {},
26
+ name: f.name ?? null,
27
+ phone: f.phone ?? null,
28
+ preferred_locales: [],
29
+ shipping: null,
30
+ tax_exempt: "none",
31
+ test_clock: null,
32
+ };
33
+ }
34
+ export function deletedStub(id, object) {
35
+ return { id, object, deleted: true };
36
+ }
37
+ export function productShape(f) {
38
+ return {
39
+ id: f.id,
40
+ object: "product",
41
+ active: f.active ?? true,
42
+ created: f.created,
43
+ default_price: null,
44
+ description: f.description ?? null,
45
+ images: [],
46
+ livemode: false,
47
+ metadata: f.metadata ?? {},
48
+ name: f.name,
49
+ package_dimensions: null,
50
+ shippable: null,
51
+ statement_descriptor: null,
52
+ tax_code: null,
53
+ unit_label: null,
54
+ updated: f.created,
55
+ url: null,
56
+ };
57
+ }
58
+ export function priceShape(f) {
59
+ const recurring = f.recurring
60
+ ? {
61
+ aggregate_usage: null,
62
+ interval: f.recurring.interval,
63
+ interval_count: f.recurring.interval_count,
64
+ trial_period_days: null,
65
+ usage_type: "licensed",
66
+ }
67
+ : null;
68
+ return {
69
+ id: f.id,
70
+ object: "price",
71
+ active: f.active ?? true,
72
+ billing_scheme: "per_unit",
73
+ created: f.created,
74
+ currency: f.currency,
75
+ custom_unit_amount: null,
76
+ livemode: false,
77
+ lookup_key: null,
78
+ metadata: f.metadata ?? {},
79
+ nickname: f.nickname ?? null,
80
+ product: f.product,
81
+ recurring,
82
+ tax_behavior: "unspecified",
83
+ tiers_mode: null,
84
+ transform_quantity: null,
85
+ type: f.recurring ? "recurring" : "one_time",
86
+ unit_amount: f.unit_amount,
87
+ unit_amount_decimal: String(f.unit_amount),
88
+ };
89
+ }
90
+ export function lineItemShape(f) {
91
+ return {
92
+ id: f.id,
93
+ object: "line_item",
94
+ amount: f.amount,
95
+ currency: f.currency,
96
+ description: f.description ?? null,
97
+ discount_amounts: [],
98
+ discountable: true,
99
+ discounts: [],
100
+ invoice_item: f.invoice_item ?? undefined,
101
+ livemode: false,
102
+ metadata: {},
103
+ period: f.period,
104
+ price: f.price ?? null,
105
+ proration: false,
106
+ proration_details: { credited_items: null },
107
+ quantity: f.quantity,
108
+ subscription: f.subscription ?? null,
109
+ type: f.type,
110
+ };
111
+ }
112
+ export function invoiceShape(f) {
113
+ const subtotal = f.lines.reduce((sum, l) => sum + l.amount, 0);
114
+ return {
115
+ id: f.id,
116
+ object: "invoice",
117
+ account_country: "US",
118
+ account_name: "worlds",
119
+ amount_due: subtotal,
120
+ amount_paid: 0,
121
+ amount_remaining: subtotal,
122
+ attempt_count: 0,
123
+ attempted: false,
124
+ auto_advance: false,
125
+ automatic_tax: { enabled: false, liability: null, status: null },
126
+ billing_reason: f.billing_reason ?? "manual",
127
+ charge: null,
128
+ collection_method: "charge_automatically",
129
+ created: f.created,
130
+ currency: f.currency,
131
+ customer: f.customer,
132
+ default_payment_method: null,
133
+ description: null,
134
+ discounts: [],
135
+ due_date: null,
136
+ ending_balance: null,
137
+ footer: null,
138
+ lines: {
139
+ object: "list",
140
+ data: f.lines,
141
+ has_more: false,
142
+ total_count: f.lines.length,
143
+ url: `/v1/invoices/${f.id}/lines`,
144
+ },
145
+ livemode: false,
146
+ metadata: f.metadata ?? {},
147
+ next_payment_attempt: null,
148
+ number: null,
149
+ paid: false,
150
+ paid_out_of_band: false,
151
+ payment_intent: null,
152
+ period_end: f.period_end ?? f.created,
153
+ period_start: f.period_start ?? f.created,
154
+ receipt_number: null,
155
+ starting_balance: 0,
156
+ status: "draft",
157
+ status_transitions: { finalized_at: null, marked_uncollectible_at: null, paid_at: null, voided_at: null },
158
+ subscription: f.subscription ?? null,
159
+ subtotal,
160
+ total: subtotal,
161
+ };
162
+ }
163
+ export function invoiceItemShape(f) {
164
+ return {
165
+ id: f.id,
166
+ object: "invoiceitem",
167
+ amount: f.amount,
168
+ currency: f.currency,
169
+ customer: f.customer,
170
+ date: f.created,
171
+ description: f.description ?? null,
172
+ discountable: true,
173
+ discounts: [],
174
+ invoice: f.invoice ?? null,
175
+ livemode: false,
176
+ metadata: f.metadata ?? {},
177
+ period: { start: f.created, end: f.created },
178
+ price: f.price ?? null,
179
+ proration: false,
180
+ quantity: f.quantity ?? 1,
181
+ subscription: null,
182
+ unit_amount: f.amount / (f.quantity ?? 1),
183
+ unit_amount_decimal: String(f.amount / (f.quantity ?? 1)),
184
+ };
185
+ }
186
+ export function paymentIntentShape(f) {
187
+ return {
188
+ id: f.id,
189
+ object: "payment_intent",
190
+ amount: f.amount,
191
+ amount_capturable: 0,
192
+ amount_received: 0,
193
+ canceled_at: null,
194
+ cancellation_reason: null,
195
+ capture_method: "automatic",
196
+ client_secret: `${f.id}_secret_${f.clientSecretToken}`,
197
+ confirmation_method: "automatic",
198
+ created: f.created,
199
+ currency: f.currency,
200
+ customer: f.customer ?? null,
201
+ description: f.description ?? null,
202
+ invoice: f.invoice ?? null,
203
+ last_payment_error: null,
204
+ latest_charge: null,
205
+ livemode: false,
206
+ metadata: f.metadata ?? {},
207
+ next_action: null,
208
+ payment_method: null,
209
+ payment_method_types: ["card"],
210
+ receipt_email: null,
211
+ status: "requires_payment_method",
212
+ };
213
+ }
214
+ export function chargeShape(f) {
215
+ const succeeded = f.status === "succeeded";
216
+ return {
217
+ id: f.id,
218
+ object: "charge",
219
+ amount: f.amount,
220
+ amount_captured: succeeded ? f.amount : 0,
221
+ amount_refunded: 0,
222
+ balance_transaction: null,
223
+ billing_details: { address: null, email: null, name: null, phone: null },
224
+ calculated_statement_descriptor: "WORLDS",
225
+ captured: succeeded,
226
+ created: f.created,
227
+ currency: f.currency,
228
+ customer: f.customer ?? null,
229
+ description: f.description ?? null,
230
+ disputed: false,
231
+ failure_code: f.failure_code ?? null,
232
+ failure_message: f.failure_message ?? null,
233
+ invoice: f.invoice ?? null,
234
+ livemode: false,
235
+ metadata: f.metadata ?? {},
236
+ outcome: succeeded
237
+ ? {
238
+ network_status: "approved_by_network",
239
+ reason: null,
240
+ risk_level: "normal",
241
+ seller_message: "Payment complete.",
242
+ type: "authorized",
243
+ }
244
+ : {
245
+ network_status: "declined_by_network",
246
+ reason: "generic_decline",
247
+ risk_level: "normal",
248
+ seller_message: "The bank did not return any further details with this decline.",
249
+ type: "issuer_declined",
250
+ },
251
+ paid: succeeded,
252
+ payment_intent: f.payment_intent ?? null,
253
+ payment_method: f.payment_method ?? null,
254
+ payment_method_details: {
255
+ card: { brand: "visa", last4: "4242", exp_month: 12, exp_year: 2044, funding: "credit", network: "visa" },
256
+ type: "card",
257
+ },
258
+ receipt_url: null,
259
+ refunded: false,
260
+ status: f.status,
261
+ };
262
+ }
263
+ export function refundShape(f) {
264
+ return {
265
+ id: f.id,
266
+ object: "refund",
267
+ amount: f.amount,
268
+ balance_transaction: null,
269
+ charge: f.charge,
270
+ created: f.created,
271
+ currency: f.currency,
272
+ metadata: f.metadata ?? {},
273
+ payment_intent: f.payment_intent ?? null,
274
+ reason: f.reason ?? null,
275
+ receipt_number: null,
276
+ source_transfer_reversal: null,
277
+ status: "succeeded",
278
+ transfer_reversal: null,
279
+ };
280
+ }
281
+ export function subscriptionShape(f) {
282
+ return {
283
+ id: f.id,
284
+ object: "subscription",
285
+ application: null,
286
+ automatic_tax: { enabled: false, liability: null },
287
+ billing_cycle_anchor: f.current_period_start,
288
+ cancel_at: null,
289
+ cancel_at_period_end: f.cancel_at_period_end ?? false,
290
+ canceled_at: null,
291
+ cancellation_details: { comment: null, feedback: null, reason: null },
292
+ collection_method: "charge_automatically",
293
+ created: f.created,
294
+ currency: f.currency,
295
+ current_period_end: f.current_period_end,
296
+ current_period_start: f.current_period_start,
297
+ customer: f.customer,
298
+ days_until_due: null,
299
+ default_payment_method: null,
300
+ description: null,
301
+ discounts: [],
302
+ ended_at: null,
303
+ items: {
304
+ object: "list",
305
+ data: [
306
+ {
307
+ id: f.itemId,
308
+ object: "subscription_item",
309
+ created: f.created,
310
+ metadata: {},
311
+ price: f.price,
312
+ quantity: f.quantity,
313
+ subscription: f.id,
314
+ },
315
+ ],
316
+ has_more: false,
317
+ total_count: 1,
318
+ url: `/v1/subscription_items?subscription=${f.id}`,
319
+ },
320
+ latest_invoice: f.latest_invoice ?? null,
321
+ livemode: false,
322
+ metadata: f.metadata ?? {},
323
+ start_date: f.created,
324
+ status: "active",
325
+ trial_end: null,
326
+ trial_start: null,
327
+ };
328
+ }
329
+ export function customerBalanceTransactionShape(f) {
330
+ return {
331
+ id: f.id,
332
+ object: "customer_balance_transaction",
333
+ amount: f.amount,
334
+ created: f.created,
335
+ credit_note: null,
336
+ currency: f.currency,
337
+ customer: f.customer,
338
+ description: f.description ?? null,
339
+ ending_balance: f.ending_balance,
340
+ invoice: null,
341
+ livemode: false,
342
+ metadata: f.metadata ?? {},
343
+ type: "adjustment",
344
+ };
345
+ }
346
+ export function isoFromClock(unixSeconds) {
347
+ return new Date(unixSeconds * 1000).toISOString().replace(/\.\d{3}Z$/, "Z");
348
+ }
349
+ export function ticketShape(f) {
350
+ const createdAt = isoFromClock(f.created);
351
+ const comments = [
352
+ { id: 1, author: f.requester.name, body: f.description, public: true, created_at: createdAt },
353
+ ];
354
+ return {
355
+ id: f.id,
356
+ created: f.created,
357
+ url: `/api/v2/tickets/${f.id}.json`,
358
+ external_id: null,
359
+ via: { channel: "email" },
360
+ created_at: createdAt,
361
+ updated_at: createdAt,
362
+ subject: f.subject,
363
+ raw_subject: f.subject,
364
+ description: f.description,
365
+ priority: f.priority ?? "normal",
366
+ status: f.status ?? "open",
367
+ requester: { name: f.requester.name, email: f.requester.email },
368
+ assignee_id: null,
369
+ tags: f.tags ?? [],
370
+ comments,
371
+ };
372
+ }
@@ -0,0 +1,32 @@
1
+ import { resourceMissing, validationError } from "../core/errors.js";
2
+ import { getInt, getString } from "./params.js";
3
+ export function paginate(items, params, url, resourceLabel) {
4
+ const limit = getInt(params, "limit") ?? 10;
5
+ if (limit < 1 || limit > 100) {
6
+ throw validationError("limit must be an integer between 1 and 100", { param: "limit" });
7
+ }
8
+ const startingAfter = getString(params, "starting_after");
9
+ const endingBefore = getString(params, "ending_before");
10
+ let data;
11
+ let hasMore;
12
+ if (startingAfter !== undefined) {
13
+ const idx = items.findIndex((i) => i.id === startingAfter);
14
+ if (idx === -1)
15
+ throw resourceMissing(startingAfter, resourceLabel, "starting_after");
16
+ data = items.slice(idx + 1, idx + 1 + limit);
17
+ hasMore = items.length > idx + 1 + limit;
18
+ }
19
+ else if (endingBefore !== undefined) {
20
+ const idx = items.findIndex((i) => i.id === endingBefore);
21
+ if (idx === -1)
22
+ throw resourceMissing(endingBefore, resourceLabel, "ending_before");
23
+ const candidates = items.slice(0, idx);
24
+ data = candidates.slice(Math.max(0, candidates.length - limit));
25
+ hasMore = candidates.length > limit;
26
+ }
27
+ else {
28
+ data = items.slice(0, limit);
29
+ hasMore = items.length > limit;
30
+ }
31
+ return { object: "list", data, has_more: hasMore, url };
32
+ }
@@ -0,0 +1,118 @@
1
+ import { validationError } from "../core/errors.js";
2
+ export function parseFormBody(raw) {
3
+ const out = {};
4
+ if (!raw)
5
+ return out;
6
+ for (const pair of raw.split("&")) {
7
+ if (!pair)
8
+ continue;
9
+ const eq = pair.indexOf("=");
10
+ const rawKey = eq === -1 ? pair : pair.slice(0, eq);
11
+ const rawValue = eq === -1 ? "" : pair.slice(eq + 1);
12
+ const key = decodeURIComponent(rawKey.replace(/\+/g, " "));
13
+ const value = decodeURIComponent(rawValue.replace(/\+/g, " "));
14
+ assignPath(out, parseKeyPath(key), value);
15
+ }
16
+ return out;
17
+ }
18
+ export function parseKeyPath(key) {
19
+ const m = /^([^[\]]+)((?:\[[^[\]]*\])*)$/.exec(key);
20
+ if (!m)
21
+ return [key];
22
+ const path = [m[1]];
23
+ const brackets = m[2];
24
+ if (brackets) {
25
+ for (const part of brackets.matchAll(/\[([^[\]]*)\]/g))
26
+ path.push(part[1]);
27
+ }
28
+ return path;
29
+ }
30
+ function assignPath(target, path, value) {
31
+ let node = target;
32
+ for (let i = 0; i < path.length; i++) {
33
+ const seg = path[i];
34
+ const last = i === path.length - 1;
35
+ const nextSeg = path[i + 1];
36
+ if (Array.isArray(node)) {
37
+ const idx = seg === "" ? node.length : Number(seg);
38
+ if (last) {
39
+ node[idx] = value;
40
+ }
41
+ else {
42
+ if (typeof node[idx] !== "object" || node[idx] === null) {
43
+ node[idx] = nextSeg !== undefined && (nextSeg === "" || /^\d+$/.test(nextSeg)) ? [] : {};
44
+ }
45
+ node = node[idx];
46
+ }
47
+ }
48
+ else {
49
+ if (last) {
50
+ node[seg] = value;
51
+ }
52
+ else {
53
+ if (typeof node[seg] !== "object" || node[seg] === null) {
54
+ node[seg] = nextSeg !== undefined && (nextSeg === "" || /^\d+$/.test(nextSeg)) ? [] : {};
55
+ }
56
+ node = node[seg];
57
+ }
58
+ }
59
+ }
60
+ }
61
+ export function getString(params, name) {
62
+ const v = params[name];
63
+ if (v === undefined || v === null)
64
+ return undefined;
65
+ if (typeof v !== "string")
66
+ throw validationError(`Invalid ${name}: must be a string`, { param: name });
67
+ return v;
68
+ }
69
+ export function requireString(params, name) {
70
+ const v = getString(params, name);
71
+ if (v === undefined || v === "")
72
+ throw validationError(`Missing required param: ${name}.`, { code: "parameter_missing", param: name });
73
+ return v;
74
+ }
75
+ export function requireCurrency(params, name = "currency") {
76
+ const currency = requireString(params, name).toLowerCase();
77
+ if (!/^[a-z]{3}$/.test(currency))
78
+ throw validationError(`Invalid currency: ${currency}`, { param: name });
79
+ return currency;
80
+ }
81
+ export function getInt(params, name) {
82
+ const v = params[name];
83
+ if (v === undefined || v === null)
84
+ return undefined;
85
+ const n = typeof v === "number" ? v : Number(v);
86
+ if (!Number.isInteger(n))
87
+ throw validationError(`Invalid integer: ${String(v)}`, { code: "parameter_invalid_integer", param: name });
88
+ return n;
89
+ }
90
+ export function requireInt(params, name) {
91
+ const v = getInt(params, name);
92
+ if (v === undefined)
93
+ throw validationError(`Missing required param: ${name}.`, { code: "parameter_missing", param: name });
94
+ return v;
95
+ }
96
+ export function getBool(params, name) {
97
+ const v = params[name];
98
+ if (v === undefined || v === null)
99
+ return undefined;
100
+ if (typeof v === "boolean")
101
+ return v;
102
+ if (v === "true")
103
+ return true;
104
+ if (v === "false")
105
+ return false;
106
+ throw validationError(`Invalid boolean: ${String(v)}`, { param: name });
107
+ }
108
+ export function getMetadata(params, name = "metadata") {
109
+ const v = params[name];
110
+ if (v === undefined || v === null)
111
+ return undefined;
112
+ if (typeof v !== "object" || Array.isArray(v))
113
+ throw validationError(`Invalid ${name}: must be a hash`, { param: name });
114
+ const out = {};
115
+ for (const [k, val] of Object.entries(v))
116
+ out[k] = String(val);
117
+ return out;
118
+ }
@@ -0,0 +1,24 @@
1
+ import { paginate } from "../pagination.js";
2
+ import { getString } from "../params.js";
3
+ import { getOr404 } from "./shared.js";
4
+ function retrieve(ctx) {
5
+ return { body: getOr404(ctx.world, "charges", ctx.pathParams.id, "charge") };
6
+ }
7
+ function list(ctx) {
8
+ const { world, params } = ctx;
9
+ let items = world.listObjects("charges").map((o) => o.data);
10
+ const customer = getString(params, "customer");
11
+ if (customer !== undefined)
12
+ items = items.filter((c) => c.customer === customer);
13
+ const paymentIntent = getString(params, "payment_intent");
14
+ if (paymentIntent !== undefined)
15
+ items = items.filter((c) => c.payment_intent === paymentIntent);
16
+ const invoice = getString(params, "invoice");
17
+ if (invoice !== undefined)
18
+ items = items.filter((c) => c.invoice === invoice);
19
+ return { body: paginate(items, params, "/v1/charges", "charge") };
20
+ }
21
+ export const chargeRoutes = [
22
+ { method: "GET", pattern: "/v1/charges", handler: list },
23
+ { method: "GET", pattern: "/v1/charges/:id", handler: retrieve },
24
+ ];
@@ -0,0 +1,151 @@
1
+ import { invalidRequest, resourceMissing, validationError } from "../../core/errors.js";
2
+ import { ACCOUNT_DEFAULT_CURRENCY, customerBalanceTransactionShape, customerShape, deletedStub } from "../build.js";
3
+ import { paginate } from "../pagination.js";
4
+ import { getInt, getMetadata, getString, requireCurrency, requireInt } from "../params.js";
5
+ import { getOr404, oneCurrencyPerBalance, refuseBesidePendingCredits } from "./shared.js";
6
+ function createBalanceTransaction(ctx) {
7
+ const { world, params, pathParams } = ctx;
8
+ const amount = requireInt(params, "amount");
9
+ if (amount === 0)
10
+ throw validationError("This value must be nonzero.", { param: "amount" });
11
+ const currency = requireCurrency(params);
12
+ const description = getString(params, "description") ?? null;
13
+ const metadata = getMetadata(params) ?? {};
14
+ const customer = getOr404(world, "customers", pathParams.id, "customer");
15
+ if (customer.currency !== null && customer.currency !== currency) {
16
+ throw oneCurrencyPerBalance(`${String(customer.id)} holds ${String(customer.currency)} and this transaction is in ${currency}`, ctx.idempotencyKey, "currency");
17
+ }
18
+ if (customer.currency === null) {
19
+ refuseBesidePendingCredits(world, customer.id, currency, `this ${currency} transaction would pin its balance to ${currency}, beside which the twin could not bank that credit`, ctx.idempotencyKey, "currency");
20
+ }
21
+ const endingBalance = customer.balance + amount;
22
+ const txn = customerBalanceTransactionShape({
23
+ id: world.nextId("cbtxn"),
24
+ created: world.clock,
25
+ amount,
26
+ currency,
27
+ customer: customer.id,
28
+ description,
29
+ ending_balance: endingBalance,
30
+ metadata,
31
+ });
32
+ world.insertObject("customer_balance_transactions", txn);
33
+ customer.balance = endingBalance;
34
+ if (customer.currency === null)
35
+ customer.currency = currency;
36
+ world.updateObject("customers", customer.id, customer);
37
+ world.emitEvent("customer.updated", customer, { idempotencyKey: ctx.idempotencyKey });
38
+ return { body: txn };
39
+ }
40
+ function retrieveBalanceTransaction(ctx) {
41
+ const { world, pathParams } = ctx;
42
+ getOr404(world, "customers", pathParams.id, "customer");
43
+ const txn = getOr404(world, "customer_balance_transactions", pathParams.txn, "customer_balance_transaction");
44
+ if (txn.customer !== pathParams.id)
45
+ throw resourceMissing(pathParams.txn, "customer_balance_transaction");
46
+ return { body: txn };
47
+ }
48
+ function listBalanceTransactions(ctx) {
49
+ const { world, params, pathParams } = ctx;
50
+ getOr404(world, "customers", pathParams.id, "customer");
51
+ const items = world
52
+ .listObjects("customer_balance_transactions")
53
+ .map((o) => o.data)
54
+ .filter((t) => t.customer === pathParams.id);
55
+ return {
56
+ body: paginate(items, params, `/v1/customers/${pathParams.id}/balance_transactions`, "customer_balance_transaction"),
57
+ };
58
+ }
59
+ function create(ctx) {
60
+ const { world, params } = ctx;
61
+ const name = getString(params, "name") ?? null;
62
+ const email = getString(params, "email") ?? null;
63
+ const description = getString(params, "description") ?? null;
64
+ const phone = getString(params, "phone") ?? null;
65
+ const balance = getInt(params, "balance") ?? 0;
66
+ const metadata = getMetadata(params) ?? {};
67
+ const customer = customerShape({
68
+ id: world.nextId("cus"),
69
+ created: world.clock,
70
+ name,
71
+ email,
72
+ description,
73
+ phone,
74
+ balance,
75
+ currency: balance === 0 ? null : ACCOUNT_DEFAULT_CURRENCY,
76
+ metadata,
77
+ });
78
+ world.insertObject("customers", customer);
79
+ world.emitEvent("customer.created", customer, { idempotencyKey: ctx.idempotencyKey });
80
+ return { body: customer };
81
+ }
82
+ function retrieve(ctx) {
83
+ const { world, pathParams } = ctx;
84
+ const id = pathParams.id;
85
+ const stored = world.getObject("customers", id, { includeDeleted: true });
86
+ if (!stored)
87
+ throw resourceMissing(id, "customer", "id");
88
+ if (stored.deleted_seq !== null)
89
+ return { body: deletedStub(id, "customer") };
90
+ return { body: stored.data };
91
+ }
92
+ function update(ctx) {
93
+ const { world, params, pathParams } = ctx;
94
+ const id = pathParams.id;
95
+ const stored = world.getObject("customers", id, { includeDeleted: true });
96
+ if (!stored)
97
+ throw resourceMissing(id, "customer", "id");
98
+ if (stored.deleted_seq !== null) {
99
+ throw invalidRequest(`Customer ${id} has been deleted.`, { param: "id" });
100
+ }
101
+ const customer = stored.data;
102
+ for (const field of ["name", "email", "description", "phone"]) {
103
+ const v = getString(params, field);
104
+ if (v !== undefined)
105
+ customer[field] = v === "" ? null : v;
106
+ }
107
+ const balance = getInt(params, "balance");
108
+ if (balance !== undefined) {
109
+ if (balance !== 0 && customer.currency === null) {
110
+ refuseBesidePendingCredits(world, id, ACCOUNT_DEFAULT_CURRENCY, `this non-zero balance would pin it to ${ACCOUNT_DEFAULT_CURRENCY}, the account's currency, beside which the twin could not bank that credit`, ctx.idempotencyKey, "balance");
111
+ customer.currency = ACCOUNT_DEFAULT_CURRENCY;
112
+ }
113
+ customer.balance = balance;
114
+ }
115
+ const metadata = getMetadata(params);
116
+ if (metadata !== undefined)
117
+ customer.metadata = { ...customer.metadata, ...metadata };
118
+ world.updateObject("customers", id, customer);
119
+ world.emitEvent("customer.updated", customer, { idempotencyKey: ctx.idempotencyKey });
120
+ return { body: customer };
121
+ }
122
+ function del(ctx) {
123
+ const { world, pathParams } = ctx;
124
+ const id = pathParams.id;
125
+ const stored = world.getObject("customers", id, { includeDeleted: true });
126
+ if (!stored)
127
+ throw resourceMissing(id, "customer", "id");
128
+ if (stored.deleted_seq === null) {
129
+ world.deleteObject("customers", id);
130
+ world.emitEvent("customer.deleted", stored.data, { idempotencyKey: ctx.idempotencyKey });
131
+ }
132
+ return { body: deletedStub(id, "customer") };
133
+ }
134
+ function list(ctx) {
135
+ const { world, params } = ctx;
136
+ let items = world.listObjects("customers").map((o) => o.data);
137
+ const email = getString(params, "email");
138
+ if (email !== undefined)
139
+ items = items.filter((c) => c.email === email);
140
+ return { body: paginate(items, params, "/v1/customers", "customer") };
141
+ }
142
+ export const customerRoutes = [
143
+ { method: "POST", pattern: "/v1/customers", handler: create },
144
+ { method: "GET", pattern: "/v1/customers", handler: list },
145
+ { method: "POST", pattern: "/v1/customers/:id/balance_transactions", handler: createBalanceTransaction },
146
+ { method: "GET", pattern: "/v1/customers/:id/balance_transactions", handler: listBalanceTransactions },
147
+ { method: "GET", pattern: "/v1/customers/:id/balance_transactions/:txn", handler: retrieveBalanceTransaction },
148
+ { method: "GET", pattern: "/v1/customers/:id", handler: retrieve },
149
+ { method: "POST", pattern: "/v1/customers/:id", handler: update },
150
+ { method: "DELETE", pattern: "/v1/customers/:id", handler: del },
151
+ ];