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,26 @@
1
+ import { resourceMissing } from "../../core/errors.js";
2
+ import { paginate } from "../pagination.js";
3
+ import { getString } from "../params.js";
4
+ function withPendingWebhooks(world, event) {
5
+ return { ...event.data, pending_webhooks: world.pendingWebhooksFor(event.id) };
6
+ }
7
+ function list(ctx) {
8
+ const { world, params } = ctx;
9
+ let events = world.listEvents().reverse();
10
+ const type = getString(params, "type");
11
+ if (type !== undefined)
12
+ events = events.filter((e) => e.type === type);
13
+ const items = events.map((e) => withPendingWebhooks(world, e));
14
+ return { body: paginate(items, params, "/v1/events", "event") };
15
+ }
16
+ function retrieve(ctx) {
17
+ const { world, pathParams } = ctx;
18
+ const event = world.getEvent(pathParams.id);
19
+ if (!event)
20
+ throw resourceMissing(pathParams.id, "event", "id");
21
+ return { body: withPendingWebhooks(world, event) };
22
+ }
23
+ export const eventRoutes = [
24
+ { method: "GET", pattern: "/v1/events", handler: list },
25
+ { method: "GET", pattern: "/v1/events/:id", handler: retrieve },
26
+ ];
@@ -0,0 +1,217 @@
1
+ import { invalidRequest, validationError } from "../../core/errors.js";
2
+ import { deletedStub, invoiceItemShape, invoiceShape, lineItemShape } from "../build.js";
3
+ import { paginate } from "../pagination.js";
4
+ import { getInt, getMetadata, getString, requireCurrency, requireInt, requireString } from "../params.js";
5
+ import { finalizeInvoiceCore, getOr404, oneCurrencyPerBalance, refuseBesidePendingCredits, settleInvoice, } from "./shared.js";
6
+ function createInvoiceItem(ctx) {
7
+ const { world, params } = ctx;
8
+ const customerId = requireString(params, "customer");
9
+ const priceId = getString(params, "price");
10
+ const requestedQuantity = getInt(params, "quantity") ?? 1;
11
+ if (requestedQuantity < 0)
12
+ throw validationError("This value must be greater than or equal to 0.", { param: "quantity" });
13
+ const bare = priceId === undefined ? { amount: requireInt(params, "amount"), currency: requireCurrency(params) } : null;
14
+ const description = getString(params, "description") ?? null;
15
+ const metadata = getMetadata(params) ?? {};
16
+ const customer = getOr404(world, "customers", customerId, "customer", "customer");
17
+ let amount;
18
+ let currency;
19
+ let quantity;
20
+ let priceObj = null;
21
+ if (bare) {
22
+ ({ amount, currency } = bare);
23
+ quantity = 1;
24
+ }
25
+ else {
26
+ priceObj = getOr404(world, "prices", priceId, "price", "price");
27
+ amount = priceObj.unit_amount * requestedQuantity;
28
+ currency = priceObj.currency;
29
+ quantity = requestedQuantity;
30
+ }
31
+ if (amount < 0) {
32
+ const param = bare ? "currency" : "price";
33
+ if (customer.currency === null) {
34
+ refuseBesidePendingCredits(world, customerId, currency, `this ${currency} credit line would be a second currency of credit, which the twin could not bank beside the first`, ctx.idempotencyKey, param);
35
+ }
36
+ else if (customer.currency !== currency) {
37
+ throw oneCurrencyPerBalance(`${customerId} holds ${String(customer.currency)} and this ${currency} credit line would become a ${currency} credit on the invoice that collects it`, ctx.idempotencyKey, param);
38
+ }
39
+ }
40
+ const item = invoiceItemShape({
41
+ id: world.nextId("ii"),
42
+ created: world.clock,
43
+ customer: customerId,
44
+ amount,
45
+ currency,
46
+ description,
47
+ quantity,
48
+ price: priceObj,
49
+ metadata,
50
+ });
51
+ world.insertObject("invoiceitems", item);
52
+ world.emitEvent("invoiceitem.created", item, { idempotencyKey: ctx.idempotencyKey });
53
+ return { body: item };
54
+ }
55
+ function retrieveInvoiceItem(ctx) {
56
+ return { body: getOr404(ctx.world, "invoiceitems", ctx.pathParams.id, "invoiceitem") };
57
+ }
58
+ function listInvoiceItems(ctx) {
59
+ const { world, params } = ctx;
60
+ let items = world.listObjects("invoiceitems").map((o) => o.data);
61
+ const customer = getString(params, "customer");
62
+ if (customer !== undefined)
63
+ items = items.filter((i) => i.customer === customer);
64
+ const pending = getString(params, "pending");
65
+ if (pending === "true")
66
+ items = items.filter((i) => i.invoice === null);
67
+ return { body: paginate(items, params, "/v1/invoiceitems", "invoiceitem") };
68
+ }
69
+ function createInvoice(ctx) {
70
+ const { world, params } = ctx;
71
+ const customerId = requireString(params, "customer");
72
+ const requestedCurrency = getString(params, "currency") === undefined ? undefined : requireCurrency(params);
73
+ const metadata = getMetadata(params) ?? {};
74
+ const description = getString(params, "description");
75
+ getOr404(world, "customers", customerId, "customer", "customer");
76
+ const pending = world
77
+ .listObjects("invoiceitems")
78
+ .map((o) => o.data)
79
+ .filter((i) => i.customer === customerId && i.invoice === null)
80
+ .reverse();
81
+ const currencies = [...new Set(pending.map((i) => i.currency))].sort();
82
+ if (requestedCurrency === undefined && currencies.length > 1) {
83
+ throw invalidRequest(`This customer has pending invoice items in more than one currency (${currencies.join(", ")}). Pass \`currency\` to say which of them this invoice collects.`, { param: "currency" });
84
+ }
85
+ const currency = requestedCurrency ?? currencies[0];
86
+ const pendingItems = pending.filter((i) => i.currency === currency);
87
+ if (pendingItems.length === 0) {
88
+ throw invalidRequest(`Nothing to invoice for customer ${customerId}.`, { code: "invoice_no_customer_line_items" });
89
+ }
90
+ const invoiceId = world.nextId("in");
91
+ const lines = pendingItems.map((item) => lineItemShape({
92
+ id: world.nextId("il"),
93
+ amount: item.amount,
94
+ currency: item.currency,
95
+ description: item.description,
96
+ invoice_item: item.id,
97
+ period: item.period,
98
+ price: item.price ?? null,
99
+ quantity: item.quantity,
100
+ type: "invoiceitem",
101
+ }));
102
+ const invoice = invoiceShape({
103
+ id: invoiceId,
104
+ created: world.clock,
105
+ customer: customerId,
106
+ currency,
107
+ lines,
108
+ metadata,
109
+ });
110
+ if (description !== undefined)
111
+ invoice.description = description;
112
+ world.insertObject("invoices", invoice);
113
+ for (const item of pendingItems) {
114
+ item.invoice = invoiceId;
115
+ world.updateObject("invoiceitems", item.id, item);
116
+ }
117
+ world.emitEvent("invoice.created", invoice, { idempotencyKey: ctx.idempotencyKey });
118
+ return { body: invoice };
119
+ }
120
+ function retrieveInvoice(ctx) {
121
+ return { body: getOr404(ctx.world, "invoices", ctx.pathParams.id, "invoice") };
122
+ }
123
+ function listInvoices(ctx) {
124
+ const { world, params } = ctx;
125
+ let items = world.listObjects("invoices").map((o) => o.data);
126
+ const customer = getString(params, "customer");
127
+ if (customer !== undefined)
128
+ items = items.filter((i) => i.customer === customer);
129
+ const status = getString(params, "status");
130
+ if (status !== undefined) {
131
+ if (!["draft", "open", "paid", "void", "uncollectible"].includes(status)) {
132
+ throw validationError(`Invalid status: ${status}`, { param: "status" });
133
+ }
134
+ items = items.filter((i) => i.status === status);
135
+ }
136
+ const subscription = getString(params, "subscription");
137
+ if (subscription !== undefined)
138
+ items = items.filter((i) => i.subscription === subscription);
139
+ return { body: paginate(items, params, "/v1/invoices", "invoice") };
140
+ }
141
+ function listInvoiceLines(ctx) {
142
+ const { world, params, pathParams } = ctx;
143
+ const invoice = getOr404(world, "invoices", pathParams.id, "invoice");
144
+ const lines = invoice.lines.data;
145
+ return { body: paginate([...lines], params, `/v1/invoices/${pathParams.id}/lines`, "line_item") };
146
+ }
147
+ function finalizeInvoice(ctx) {
148
+ const { world, pathParams } = ctx;
149
+ const invoice = getOr404(world, "invoices", pathParams.id, "invoice");
150
+ if (invoice.status !== "draft") {
151
+ throw invalidRequest(`This invoice can't be finalized because it's not a draft. You can only finalize draft invoices.`, { code: "invoice_not_editable" });
152
+ }
153
+ finalizeInvoiceCore(world, invoice, { idempotencyKey: ctx.idempotencyKey });
154
+ return { body: invoice };
155
+ }
156
+ function payInvoice(ctx) {
157
+ const { world, pathParams } = ctx;
158
+ const invoice = getOr404(world, "invoices", pathParams.id, "invoice");
159
+ if (invoice.status === "paid") {
160
+ throw invalidRequest(`Invoice ${invoice.id} is already paid.`, { code: "invoice_already_paid" });
161
+ }
162
+ if (invoice.status !== "open") {
163
+ throw invalidRequest(`This invoice can't be paid because its status is ${invoice.status}. Finalize the invoice first.`, { code: "invoice_not_payable" });
164
+ }
165
+ settleInvoice(world, invoice, { idempotencyKey: ctx.idempotencyKey });
166
+ return { body: invoice };
167
+ }
168
+ function voidInvoice(ctx) {
169
+ const { world, pathParams } = ctx;
170
+ const invoice = getOr404(world, "invoices", pathParams.id, "invoice");
171
+ if (invoice.status !== "open" && invoice.status !== "uncollectible") {
172
+ throw invalidRequest(`You cannot void this invoice because its status is ${invoice.status}. Only open or uncollectible invoices can be voided.`, {
173
+ code: "invoice_not_voidable",
174
+ });
175
+ }
176
+ invoice.status = "void";
177
+ invoice.status_transitions.voided_at = world.clock;
178
+ world.updateObject("invoices", invoice.id, invoice);
179
+ world.emitEvent("invoice.voided", invoice, { idempotencyKey: ctx.idempotencyKey });
180
+ return { body: invoice };
181
+ }
182
+ function markUncollectible(ctx) {
183
+ const { world, pathParams } = ctx;
184
+ const invoice = getOr404(world, "invoices", pathParams.id, "invoice");
185
+ if (invoice.status !== "open") {
186
+ throw invalidRequest(`Only open invoices can be marked uncollectible (status is ${invoice.status}).`);
187
+ }
188
+ invoice.status = "uncollectible";
189
+ invoice.status_transitions.marked_uncollectible_at = world.clock;
190
+ world.updateObject("invoices", invoice.id, invoice);
191
+ world.emitEvent("invoice.marked_uncollectible", invoice, { idempotencyKey: ctx.idempotencyKey });
192
+ return { body: invoice };
193
+ }
194
+ function deleteInvoice(ctx) {
195
+ const { world, pathParams } = ctx;
196
+ const invoice = getOr404(world, "invoices", pathParams.id, "invoice");
197
+ if (invoice.status !== "draft") {
198
+ throw invalidRequest(`You can only delete draft invoices. To remove this invoice from your books, void it instead.`);
199
+ }
200
+ world.deleteObject("invoices", pathParams.id);
201
+ world.emitEvent("invoice.deleted", invoice, { idempotencyKey: ctx.idempotencyKey });
202
+ return { body: deletedStub(pathParams.id, "invoice") };
203
+ }
204
+ export const invoiceRoutes = [
205
+ { method: "POST", pattern: "/v1/invoiceitems", handler: createInvoiceItem },
206
+ { method: "GET", pattern: "/v1/invoiceitems", handler: listInvoiceItems },
207
+ { method: "GET", pattern: "/v1/invoiceitems/:id", handler: retrieveInvoiceItem },
208
+ { method: "POST", pattern: "/v1/invoices", handler: createInvoice },
209
+ { method: "GET", pattern: "/v1/invoices", handler: listInvoices },
210
+ { method: "GET", pattern: "/v1/invoices/:id", handler: retrieveInvoice },
211
+ { method: "DELETE", pattern: "/v1/invoices/:id", handler: deleteInvoice },
212
+ { method: "GET", pattern: "/v1/invoices/:id/lines", handler: listInvoiceLines },
213
+ { method: "POST", pattern: "/v1/invoices/:id/finalize", handler: finalizeInvoice },
214
+ { method: "POST", pattern: "/v1/invoices/:id/pay", handler: payInvoice },
215
+ { method: "POST", pattern: "/v1/invoices/:id/void", handler: voidInvoice },
216
+ { method: "POST", pattern: "/v1/invoices/:id/mark_uncollectible", handler: markUncollectible },
217
+ ];
@@ -0,0 +1,146 @@
1
+ import { cardError, invalidRequest, resourceMissing, validationError } from "../../core/errors.js";
2
+ import { chargeShape, paymentIntentShape } from "../build.js";
3
+ import { paginate } from "../pagination.js";
4
+ import { getBool, getMetadata, getString, requireCurrency, requireInt } from "../params.js";
5
+ import { getOr404 } from "./shared.js";
6
+ const TEST_PAYMENT_METHODS = new Set(["pm_card_visa", "pm_card_chargeDeclined"]);
7
+ function create(ctx) {
8
+ const { world, params } = ctx;
9
+ const amount = requireInt(params, "amount");
10
+ if (amount < 1)
11
+ throw validationError("This value must be greater than or equal to 1.", { param: "amount" });
12
+ const currency = requireCurrency(params);
13
+ const customerId = getString(params, "customer");
14
+ const description = getString(params, "description") ?? null;
15
+ const metadata = getMetadata(params) ?? {};
16
+ const confirm = getBool(params, "confirm") ?? false;
17
+ const paymentMethod = getString(params, "payment_method");
18
+ if (customerId !== undefined)
19
+ getOr404(world, "customers", customerId, "customer", "customer");
20
+ const pi = paymentIntentShape({
21
+ id: world.nextId("pi"),
22
+ created: world.clock,
23
+ amount,
24
+ currency,
25
+ customer: customerId ?? null,
26
+ description,
27
+ metadata,
28
+ clientSecretToken: world.nextToken(24),
29
+ });
30
+ world.insertObject("payment_intents", pi);
31
+ world.emitEvent("payment_intent.created", pi, { idempotencyKey: ctx.idempotencyKey });
32
+ if (confirm) {
33
+ return confirmPaymentIntent(ctx, pi, paymentMethod);
34
+ }
35
+ return { body: pi };
36
+ }
37
+ function confirmPaymentIntent(ctx, pi, pm) {
38
+ const { world } = ctx;
39
+ if (pi.status === "succeeded") {
40
+ throw invalidRequest(`This PaymentIntent's state is incompatible with the operation you were trying to perform. The PaymentIntent has a status of succeeded.`, { code: "payment_intent_unexpected_state" });
41
+ }
42
+ if (pi.status === "canceled") {
43
+ throw invalidRequest(`This PaymentIntent's state is incompatible with the operation you were trying to perform. The PaymentIntent has a status of canceled.`, { code: "payment_intent_unexpected_state" });
44
+ }
45
+ if (pm === undefined) {
46
+ throw invalidRequest("You cannot confirm this PaymentIntent because it's missing a payment method.", {
47
+ code: "payment_intent_payment_attempt_failed",
48
+ });
49
+ }
50
+ if (!TEST_PAYMENT_METHODS.has(pm)) {
51
+ throw resourceMissing(pm, "PaymentMethod", "payment_method");
52
+ }
53
+ pi.payment_method = pm;
54
+ if (pm === "pm_card_chargeDeclined") {
55
+ const charge = chargeShape({
56
+ id: world.nextId("ch"),
57
+ created: world.clock,
58
+ amount: pi.amount,
59
+ currency: pi.currency,
60
+ customer: pi.customer,
61
+ payment_intent: pi.id,
62
+ payment_method: pm,
63
+ status: "failed",
64
+ failure_code: "card_declined",
65
+ failure_message: "Your card was declined.",
66
+ description: pi.description,
67
+ });
68
+ world.insertObject("charges", charge);
69
+ pi.latest_charge = charge.id;
70
+ pi.status = "requires_payment_method";
71
+ pi.last_payment_error = {
72
+ charge: charge.id,
73
+ code: "card_declined",
74
+ decline_code: "generic_decline",
75
+ doc_url: "https://stripe.com/docs/error-codes/card-declined",
76
+ message: "Your card was declined.",
77
+ payment_method: { id: pm, object: "payment_method", type: "card" },
78
+ type: "card_error",
79
+ };
80
+ world.updateObject("payment_intents", pi.id, pi);
81
+ world.emitEvent("charge.failed", charge, { idempotencyKey: ctx.idempotencyKey });
82
+ world.emitEvent("payment_intent.payment_failed", pi, { idempotencyKey: ctx.idempotencyKey });
83
+ const err = cardError("card_declined", "Your card was declined.", { declineCode: "generic_decline" });
84
+ const body = err.toJSON();
85
+ body.error = { ...body.error, charge: charge.id, payment_intent: pi };
86
+ return { status: err.status, body };
87
+ }
88
+ const charge = chargeShape({
89
+ id: world.nextId("ch"),
90
+ created: world.clock,
91
+ amount: pi.amount,
92
+ currency: pi.currency,
93
+ customer: pi.customer,
94
+ payment_intent: pi.id,
95
+ payment_method: pm,
96
+ status: "succeeded",
97
+ description: pi.description,
98
+ });
99
+ world.insertObject("charges", charge);
100
+ pi.status = "succeeded";
101
+ pi.amount_received = pi.amount;
102
+ pi.latest_charge = charge.id;
103
+ pi.last_payment_error = null;
104
+ world.updateObject("payment_intents", pi.id, pi);
105
+ world.emitEvent("charge.succeeded", charge, { idempotencyKey: ctx.idempotencyKey });
106
+ world.emitEvent("payment_intent.succeeded", pi, { idempotencyKey: ctx.idempotencyKey });
107
+ return { body: pi };
108
+ }
109
+ function confirm(ctx) {
110
+ const pm = getString(ctx.params, "payment_method");
111
+ const pi = getOr404(ctx.world, "payment_intents", ctx.pathParams.id, "payment_intent");
112
+ return confirmPaymentIntent(ctx, pi, pm);
113
+ }
114
+ function cancel(ctx) {
115
+ const { world, pathParams } = ctx;
116
+ const pi = getOr404(world, "payment_intents", pathParams.id, "payment_intent");
117
+ if (pi.status !== "requires_payment_method") {
118
+ throw invalidRequest(`You cannot cancel this PaymentIntent because it has a status of ${pi.status}.`, {
119
+ code: "payment_intent_unexpected_state",
120
+ });
121
+ }
122
+ pi.status = "canceled";
123
+ pi.canceled_at = world.clock;
124
+ pi.cancellation_reason = getString(ctx.params, "cancellation_reason") ?? "requested_by_customer";
125
+ world.updateObject("payment_intents", pi.id, pi);
126
+ world.emitEvent("payment_intent.canceled", pi, { idempotencyKey: ctx.idempotencyKey });
127
+ return { body: pi };
128
+ }
129
+ function retrieve(ctx) {
130
+ return { body: getOr404(ctx.world, "payment_intents", ctx.pathParams.id, "payment_intent") };
131
+ }
132
+ function list(ctx) {
133
+ const { world, params } = ctx;
134
+ let items = world.listObjects("payment_intents").map((o) => o.data);
135
+ const customer = getString(params, "customer");
136
+ if (customer !== undefined)
137
+ items = items.filter((p) => p.customer === customer);
138
+ return { body: paginate(items, params, "/v1/payment_intents", "payment_intent") };
139
+ }
140
+ export const paymentIntentRoutes = [
141
+ { method: "POST", pattern: "/v1/payment_intents", handler: create },
142
+ { method: "GET", pattern: "/v1/payment_intents", handler: list },
143
+ { method: "GET", pattern: "/v1/payment_intents/:id", handler: retrieve },
144
+ { method: "POST", pattern: "/v1/payment_intents/:id/confirm", handler: confirm },
145
+ { method: "POST", pattern: "/v1/payment_intents/:id/cancel", handler: cancel },
146
+ ];
@@ -0,0 +1,92 @@
1
+ import { validationError } from "../../core/errors.js";
2
+ import { priceShape, productShape } from "../build.js";
3
+ import { paginate } from "../pagination.js";
4
+ import { getBool, getInt, getMetadata, getString, requireCurrency, requireInt, requireString } from "../params.js";
5
+ import { getOr404 } from "./shared.js";
6
+ function createProduct(ctx) {
7
+ const { world, params } = ctx;
8
+ const product = productShape({
9
+ id: world.nextId("prod"),
10
+ created: world.clock,
11
+ name: requireString(params, "name"),
12
+ description: getString(params, "description") ?? null,
13
+ active: getBool(params, "active") ?? true,
14
+ metadata: getMetadata(params) ?? {},
15
+ });
16
+ world.insertObject("products", product);
17
+ world.emitEvent("product.created", product, { idempotencyKey: ctx.idempotencyKey });
18
+ return { body: product };
19
+ }
20
+ function retrieveProduct(ctx) {
21
+ return { body: getOr404(ctx.world, "products", ctx.pathParams.id, "product") };
22
+ }
23
+ function listProducts(ctx) {
24
+ const { world, params } = ctx;
25
+ let items = world.listObjects("products").map((o) => o.data);
26
+ const active = getBool(params, "active");
27
+ if (active !== undefined)
28
+ items = items.filter((p) => p.active === active);
29
+ return { body: paginate(items, params, "/v1/products", "product") };
30
+ }
31
+ function createPrice(ctx) {
32
+ const { world, params } = ctx;
33
+ const productId = requireString(params, "product");
34
+ const unitAmount = requireInt(params, "unit_amount");
35
+ if (unitAmount < 0)
36
+ throw validationError("This value must be greater than or equal to 0.", { param: "unit_amount" });
37
+ const currency = requireCurrency(params);
38
+ let recurring = null;
39
+ const rec = params.recurring;
40
+ if (rec !== undefined) {
41
+ const interval = getString(rec, "interval");
42
+ if (!interval || !["day", "week", "month", "year"].includes(interval)) {
43
+ throw validationError("Invalid recurring[interval]: must be one of day, week, month, or year", {
44
+ param: "recurring[interval]",
45
+ });
46
+ }
47
+ recurring = {
48
+ interval: interval,
49
+ interval_count: getInt(rec, "interval_count") ?? 1,
50
+ };
51
+ }
52
+ const nickname = getString(params, "nickname") ?? null;
53
+ const active = getBool(params, "active") ?? true;
54
+ const metadata = getMetadata(params) ?? {};
55
+ getOr404(world, "products", productId, "product", "product");
56
+ const price = priceShape({
57
+ id: world.nextId("price"),
58
+ created: world.clock,
59
+ product: productId,
60
+ unit_amount: unitAmount,
61
+ currency,
62
+ nickname,
63
+ active,
64
+ recurring,
65
+ metadata,
66
+ });
67
+ world.insertObject("prices", price);
68
+ world.emitEvent("price.created", price, { idempotencyKey: ctx.idempotencyKey });
69
+ return { body: price };
70
+ }
71
+ function retrievePrice(ctx) {
72
+ return { body: getOr404(ctx.world, "prices", ctx.pathParams.id, "price") };
73
+ }
74
+ function listPrices(ctx) {
75
+ const { world, params } = ctx;
76
+ let items = world.listObjects("prices").map((o) => o.data);
77
+ const product = getString(params, "product");
78
+ if (product !== undefined)
79
+ items = items.filter((p) => p.product === product);
80
+ const active = getBool(params, "active");
81
+ if (active !== undefined)
82
+ items = items.filter((p) => p.active === active);
83
+ return { body: paginate(items, params, "/v1/prices", "price") };
84
+ }
85
+ export const productRoutes = [
86
+ { method: "POST", pattern: "/v1/products", handler: createProduct },
87
+ { method: "GET", pattern: "/v1/products", handler: listProducts },
88
+ { method: "GET", pattern: "/v1/products/:id", handler: retrieveProduct },
89
+ { method: "POST", pattern: "/v1/prices", handler: createPrice },
90
+ { method: "GET", pattern: "/v1/prices", handler: listPrices },
91
+ { method: "GET", pattern: "/v1/prices/:id", handler: retrievePrice },
92
+ ];
@@ -0,0 +1,103 @@
1
+ import { invalidRequest, resourceMissing, validationError } from "../../core/errors.js";
2
+ import { refundShape } from "../build.js";
3
+ import { paginate } from "../pagination.js";
4
+ import { getInt, getMetadata, getString, requireCurrency } from "../params.js";
5
+ import { getOr404 } from "./shared.js";
6
+ function formatAmount(cents, currency) {
7
+ const symbol = currency === "usd" ? "$" : currency === "eur" ? "€" : currency === "gbp" ? "£" : `${currency.toUpperCase()} `;
8
+ return `${symbol}${(cents / 100).toFixed(2)}`;
9
+ }
10
+ function create(ctx) {
11
+ const { world, params } = ctx;
12
+ const chargeId = getString(params, "charge");
13
+ const paymentIntentId = getString(params, "payment_intent");
14
+ if (chargeId === undefined && paymentIntentId === undefined) {
15
+ throw validationError("One of the following params should be provided for this request: payment_intent or charge.", {
16
+ code: "parameter_missing",
17
+ });
18
+ }
19
+ if (chargeId !== undefined && paymentIntentId !== undefined) {
20
+ throw validationError("Only one of the following params may be provided for this request: payment_intent or charge.");
21
+ }
22
+ const requestedAmount = getInt(params, "amount");
23
+ if (requestedAmount !== undefined && requestedAmount < 1)
24
+ throw validationError("This value must be greater than or equal to 1.", { param: "amount" });
25
+ const reason = getString(params, "reason");
26
+ if (reason !== undefined && !["duplicate", "fraudulent", "requested_by_customer"].includes(reason)) {
27
+ throw validationError(`Invalid reason: must be one of duplicate, fraudulent, or requested_by_customer`, {
28
+ param: "reason",
29
+ });
30
+ }
31
+ const metadata = getMetadata(params) ?? {};
32
+ const currency = params.currency === undefined || params.currency === "" ? undefined : requireCurrency(params);
33
+ let charge;
34
+ if (chargeId !== undefined) {
35
+ charge = getOr404(world, "charges", chargeId, "charge", "charge");
36
+ }
37
+ else {
38
+ const pi = getOr404(world, "payment_intents", paymentIntentId, "payment_intent", "payment_intent");
39
+ const latest = pi.latest_charge;
40
+ if (!latest) {
41
+ throw invalidRequest(`This PaymentIntent does not have a successful charge to refund.`, {
42
+ param: "payment_intent",
43
+ });
44
+ }
45
+ const stored = world.getObject("charges", latest);
46
+ if (!stored)
47
+ throw resourceMissing(latest, "charge");
48
+ charge = stored.data;
49
+ }
50
+ if (charge.status !== "succeeded") {
51
+ throw invalidRequest(`The charge ${charge.id} has failed, and thus cannot be refunded.`, { param: "charge" });
52
+ }
53
+ if (currency !== undefined && currency !== charge.currency) {
54
+ throw invalidRequest(`The refund's currency (${currency}) must match the currency of the charge (${String(charge.currency)}).`, { param: "currency" });
55
+ }
56
+ const alreadyRefunded = charge.amount_refunded;
57
+ const chargeAmount = charge.amount;
58
+ const remaining = chargeAmount - alreadyRefunded;
59
+ if (remaining <= 0) {
60
+ throw invalidRequest(`Charge ${charge.id} has already been refunded.`, { code: "charge_already_refunded" });
61
+ }
62
+ const amount = requestedAmount ?? remaining;
63
+ if (amount > remaining) {
64
+ const currency = charge.currency;
65
+ throw invalidRequest(`Refund amount (${formatAmount(amount, currency)}) is greater than unrefunded amount on charge (${formatAmount(remaining, currency)})`, { param: "amount" });
66
+ }
67
+ const refund = refundShape({
68
+ id: world.nextId("re"),
69
+ created: world.clock,
70
+ amount,
71
+ currency: charge.currency,
72
+ charge: charge.id,
73
+ payment_intent: charge.payment_intent ?? null,
74
+ reason: reason ?? null,
75
+ metadata,
76
+ });
77
+ world.insertObject("refunds", refund);
78
+ charge.amount_refunded = alreadyRefunded + amount;
79
+ charge.refunded = charge.amount_refunded === chargeAmount;
80
+ world.updateObject("charges", charge.id, charge);
81
+ world.emitEvent("charge.refunded", charge, { idempotencyKey: ctx.idempotencyKey });
82
+ world.emitEvent("refund.created", refund, { idempotencyKey: ctx.idempotencyKey });
83
+ return { body: refund };
84
+ }
85
+ function retrieve(ctx) {
86
+ return { body: getOr404(ctx.world, "refunds", ctx.pathParams.id, "refund") };
87
+ }
88
+ function list(ctx) {
89
+ const { world, params } = ctx;
90
+ let items = world.listObjects("refunds").map((o) => o.data);
91
+ const charge = getString(params, "charge");
92
+ if (charge !== undefined)
93
+ items = items.filter((r) => r.charge === charge);
94
+ const paymentIntent = getString(params, "payment_intent");
95
+ if (paymentIntent !== undefined)
96
+ items = items.filter((r) => r.payment_intent === paymentIntent);
97
+ return { body: paginate(items, params, "/v1/refunds", "refund") };
98
+ }
99
+ export const refundRoutes = [
100
+ { method: "POST", pattern: "/v1/refunds", handler: create },
101
+ { method: "GET", pattern: "/v1/refunds", handler: list },
102
+ { method: "GET", pattern: "/v1/refunds/:id", handler: retrieve },
103
+ ];