@stardeck-customer-apps/testing 0.21.0 → 0.22.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/SKILL.md +22 -3
- package/dist/index.d.mts +34 -1
- package/dist/index.d.ts +34 -1
- package/dist/index.js +360 -2
- package/dist/index.mjs +360 -2
- package/dist/next/headers-shim.js +5 -0
- package/dist/next/headers-shim.mjs +5 -0
- package/dist/setup.js +309 -2
- package/dist/setup.mjs +309 -2
- package/package.json +2 -2
package/SKILL.md
CHANGED
|
@@ -421,6 +421,24 @@ externalId })` helper models a trusted platform/channel link, and
|
|
|
421
421
|
`isVirtual: false`; create/list validation matches the control plane
|
|
422
422
|
(expiry 90–600s, amount positive int, payment-method enum, deploymentId UUID,
|
|
423
423
|
list `limit` 1–100, status filter values).
|
|
424
|
+
Beam direct charges (inline checkout): `.directCharges`, `.getDirectCharge(id)`,
|
|
425
|
+
`.completeDirectCharge(id)` and `.failDirectCharge(id, failureCode?)` settle a
|
|
426
|
+
PENDING charge and return the `charge.succeeded` / `charge.failed` payload to
|
|
427
|
+
pass to `.deliverBeamEvent`. `QR_PROMPT_PAY` returns `ENCODED_IMAGE` (expiry
|
|
428
|
+
honours `expiryDurationInSec`), wallets and mobile banking return `REDIRECT`
|
|
429
|
+
to `https://beam.stardeck.test/pay/<id>?returnUrl=…`, and the same
|
|
430
|
+
`idempotencyKey` returns the same charge. Card tokens: `tokenizeBeamCard`
|
|
431
|
+
against the simulated Beam host returns `ctok_sim_<n>`; the card number picks
|
|
432
|
+
the outcome of a `CARD_TOKEN` charge — `4111111111111111` / `5372074248113841`
|
|
433
|
+
succeed (`NONE`), `4953261730509988` / `4389783280144506` need OTP
|
|
434
|
+
(`REDIRECT`), `4943129900084541` fails `CH_INSUFFICIENT_FUNDS`,
|
|
435
|
+
`4111111111000025` fails `CH_CARD_DECLINED`, `4111111111000033` fails
|
|
436
|
+
`CH_CARD_RESTRICTED`, anything else fails `CH_PROCESSING_FAILED`.
|
|
437
|
+
`.setCardTokenization(false)` models a merchant with no publishable key
|
|
438
|
+
(`getCardTokenizationConfig` throws `CARD_TOKENIZATION_NOT_CONFIGURED`).
|
|
439
|
+
Raw `CARD` / `CARD_INSTALLMENTS`, token methods without a `cardToken`, and
|
|
440
|
+
non-QR methods without a `returnUrl` are
|
|
441
|
+
refused with 400, as on the control plane.
|
|
424
442
|
- `app.storage` — uploads through storage-sdk: `.uploads`, `.latest()`, `.count`, `.clear()`.
|
|
425
443
|
- `app.messages` — outbound Slack/LINE/Facebook sends:
|
|
426
444
|
`.all()`, `.latest()`, `.to(recipient)`, `.channel("line")`, `.count`, `.clear()`. LINE Flex
|
|
@@ -497,8 +515,8 @@ true })` remains source-compatible but the simulator (like the control plane)
|
|
|
497
515
|
- For merge-correct reads, request aliases in batches and include every id in
|
|
498
516
|
the returned canonical set. Page directory UIs through `search({ query,
|
|
499
517
|
cursor, limit })`; `list()` intentionally keeps its previous array shape.
|
|
500
|
-
- `PaymentsServerClient` (Stripe checkout + Beam payment links +
|
|
501
|
-
connections/intents/charges, product list) — captured in `app.payments`;
|
|
518
|
+
- `PaymentsServerClient` (Stripe checkout + Beam payment links + Beam direct
|
|
519
|
+
charges and card tokenization + Bolt+ connections/intents/charges, product list) — captured in `app.payments`;
|
|
502
520
|
fulfill checkout via `markPaid` (poll) or `deliverStripeEvent` /
|
|
503
521
|
`deliverBeamEvent` (webhook push). Drive a terminal without hardware via
|
|
504
522
|
`approveBoltIntent` / `declineBoltIntent` / `expireBoltIntent`. Beam payment-link
|
|
@@ -512,7 +530,8 @@ cursor, limit })`; `list()` intentionally keeps its previous array shape.
|
|
|
512
530
|
one-shot HTTP or network failures during Stripe checkout creation/status polling
|
|
513
531
|
and Beam payment-link creation/status polling. The operation names are
|
|
514
532
|
`stripe.createCheckout`, `stripe.getCheckoutSessionStatus`,
|
|
515
|
-
`beam.createPaymentLink`,
|
|
533
|
+
`beam.createPaymentLink`, `beam.getPaymentLinkStatus`, `beam.createDirectCharge`,
|
|
534
|
+
and `beam.getCharge`. HTTP failures support
|
|
516
535
|
statuses 400, 402, 409, 429, 500, and 503 plus optional `code`, `details`, and
|
|
517
536
|
`retryAfterSeconds`; `{ kind: "network", message }` rejects fetch instead.
|
|
518
537
|
`app.reset()` clears pending failures.
|
package/dist/index.d.mts
CHANGED
|
@@ -210,7 +210,7 @@ interface CapturedCheckout {
|
|
|
210
210
|
metadata?: Record<string, string>;
|
|
211
211
|
createdAt: Date;
|
|
212
212
|
}
|
|
213
|
-
type PaymentSimulatorOperation = "stripe.createCheckout" | "stripe.getCheckoutSessionStatus" | "beam.createPaymentLink" | "beam.getPaymentLinkStatus";
|
|
213
|
+
type PaymentSimulatorOperation = "stripe.createCheckout" | "stripe.getCheckoutSessionStatus" | "beam.createPaymentLink" | "beam.getPaymentLinkStatus" | "beam.createDirectCharge" | "beam.getCharge";
|
|
214
214
|
type PaymentSimulatorFailure = {
|
|
215
215
|
kind?: "http";
|
|
216
216
|
status: 400 | 402 | 409 | 429 | 500 | 503;
|
|
@@ -269,6 +269,26 @@ interface SimBoltCharge {
|
|
|
269
269
|
sourceId?: string;
|
|
270
270
|
createdAt?: string;
|
|
271
271
|
}
|
|
272
|
+
/** A direct charge (`createDirectCharge`) as the simulator holds it. */
|
|
273
|
+
interface SimDirectCharge {
|
|
274
|
+
id: string;
|
|
275
|
+
status: "SUCCEEDED" | "FAILED" | "PENDING";
|
|
276
|
+
amount: number;
|
|
277
|
+
currency: string;
|
|
278
|
+
paymentMethodType: string;
|
|
279
|
+
referenceId: string | null;
|
|
280
|
+
deviceType: string | null;
|
|
281
|
+
returnUrl: string | null;
|
|
282
|
+
actionRequired: "NONE" | "REDIRECT" | "ENCODED_IMAGE";
|
|
283
|
+
redirectUrl: string | null;
|
|
284
|
+
/** When the QR (or the charge) stops being payable. */
|
|
285
|
+
expiresAt: string;
|
|
286
|
+
failureCode: string | null;
|
|
287
|
+
/** Last four digits of a CARD_TOKEN charge's card. */
|
|
288
|
+
cardLast4: string | null;
|
|
289
|
+
idempotencyKey: string | null;
|
|
290
|
+
createdAt: string;
|
|
291
|
+
}
|
|
272
292
|
interface TestPayments {
|
|
273
293
|
get checkouts(): CapturedCheckout[];
|
|
274
294
|
latest(): CapturedCheckout | undefined;
|
|
@@ -317,6 +337,19 @@ interface TestPayments {
|
|
|
317
337
|
expireBoltIntent(intentId: string): void;
|
|
318
338
|
listBoltIntents(): SimBoltIntentRecord[];
|
|
319
339
|
getBoltIntent(intentId: string): SimBoltIntentRecord | undefined;
|
|
340
|
+
/** Every direct charge created so far, oldest first. */
|
|
341
|
+
get directCharges(): SimDirectCharge[];
|
|
342
|
+
getDirectCharge(chargeId: string): SimDirectCharge | undefined;
|
|
343
|
+
/**
|
|
344
|
+
* The shopper paid: mark a PENDING direct charge SUCCEEDED and return the
|
|
345
|
+
* `charge.succeeded` payload. Delivers no webhook; pass the payload to
|
|
346
|
+
* `deliverBeamEvent` the way Beam would.
|
|
347
|
+
*/
|
|
348
|
+
completeDirectCharge(chargeId: string): Record<string, unknown>;
|
|
349
|
+
/** Mark a PENDING direct charge FAILED; returns the `charge.failed` payload. */
|
|
350
|
+
failDirectCharge(chargeId: string, failureCode?: string): Record<string, unknown>;
|
|
351
|
+
/** Turn card tokenization off, as for a merchant with no publishable key. On by default. */
|
|
352
|
+
setCardTokenization(enabled: boolean): void;
|
|
320
353
|
clear(): void;
|
|
321
354
|
get count(): number;
|
|
322
355
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -210,7 +210,7 @@ interface CapturedCheckout {
|
|
|
210
210
|
metadata?: Record<string, string>;
|
|
211
211
|
createdAt: Date;
|
|
212
212
|
}
|
|
213
|
-
type PaymentSimulatorOperation = "stripe.createCheckout" | "stripe.getCheckoutSessionStatus" | "beam.createPaymentLink" | "beam.getPaymentLinkStatus";
|
|
213
|
+
type PaymentSimulatorOperation = "stripe.createCheckout" | "stripe.getCheckoutSessionStatus" | "beam.createPaymentLink" | "beam.getPaymentLinkStatus" | "beam.createDirectCharge" | "beam.getCharge";
|
|
214
214
|
type PaymentSimulatorFailure = {
|
|
215
215
|
kind?: "http";
|
|
216
216
|
status: 400 | 402 | 409 | 429 | 500 | 503;
|
|
@@ -269,6 +269,26 @@ interface SimBoltCharge {
|
|
|
269
269
|
sourceId?: string;
|
|
270
270
|
createdAt?: string;
|
|
271
271
|
}
|
|
272
|
+
/** A direct charge (`createDirectCharge`) as the simulator holds it. */
|
|
273
|
+
interface SimDirectCharge {
|
|
274
|
+
id: string;
|
|
275
|
+
status: "SUCCEEDED" | "FAILED" | "PENDING";
|
|
276
|
+
amount: number;
|
|
277
|
+
currency: string;
|
|
278
|
+
paymentMethodType: string;
|
|
279
|
+
referenceId: string | null;
|
|
280
|
+
deviceType: string | null;
|
|
281
|
+
returnUrl: string | null;
|
|
282
|
+
actionRequired: "NONE" | "REDIRECT" | "ENCODED_IMAGE";
|
|
283
|
+
redirectUrl: string | null;
|
|
284
|
+
/** When the QR (or the charge) stops being payable. */
|
|
285
|
+
expiresAt: string;
|
|
286
|
+
failureCode: string | null;
|
|
287
|
+
/** Last four digits of a CARD_TOKEN charge's card. */
|
|
288
|
+
cardLast4: string | null;
|
|
289
|
+
idempotencyKey: string | null;
|
|
290
|
+
createdAt: string;
|
|
291
|
+
}
|
|
272
292
|
interface TestPayments {
|
|
273
293
|
get checkouts(): CapturedCheckout[];
|
|
274
294
|
latest(): CapturedCheckout | undefined;
|
|
@@ -317,6 +337,19 @@ interface TestPayments {
|
|
|
317
337
|
expireBoltIntent(intentId: string): void;
|
|
318
338
|
listBoltIntents(): SimBoltIntentRecord[];
|
|
319
339
|
getBoltIntent(intentId: string): SimBoltIntentRecord | undefined;
|
|
340
|
+
/** Every direct charge created so far, oldest first. */
|
|
341
|
+
get directCharges(): SimDirectCharge[];
|
|
342
|
+
getDirectCharge(chargeId: string): SimDirectCharge | undefined;
|
|
343
|
+
/**
|
|
344
|
+
* The shopper paid: mark a PENDING direct charge SUCCEEDED and return the
|
|
345
|
+
* `charge.succeeded` payload. Delivers no webhook; pass the payload to
|
|
346
|
+
* `deliverBeamEvent` the way Beam would.
|
|
347
|
+
*/
|
|
348
|
+
completeDirectCharge(chargeId: string): Record<string, unknown>;
|
|
349
|
+
/** Mark a PENDING direct charge FAILED; returns the `charge.failed` payload. */
|
|
350
|
+
failDirectCharge(chargeId: string, failureCode?: string): Record<string, unknown>;
|
|
351
|
+
/** Turn card tokenization off, as for a merchant with no publishable key. On by default. */
|
|
352
|
+
setCardTokenization(enabled: boolean): void;
|
|
320
353
|
clear(): void;
|
|
321
354
|
get count(): number;
|
|
322
355
|
}
|
package/dist/index.js
CHANGED
|
@@ -348,6 +348,11 @@ var state = globalSingleton("state", () => ({
|
|
|
348
348
|
boltIntentCounter: 0,
|
|
349
349
|
boltCharges: [],
|
|
350
350
|
boltChargeCounter: 0,
|
|
351
|
+
directCharges: /* @__PURE__ */ new Map(),
|
|
352
|
+
directChargeCounter: 0,
|
|
353
|
+
cardTokens: /* @__PURE__ */ new Map(),
|
|
354
|
+
cardTokenCounter: 0,
|
|
355
|
+
cardTokenizationEnabled: true,
|
|
351
356
|
uploads: [],
|
|
352
357
|
uploadCounter: 0,
|
|
353
358
|
storageFiles: /* @__PURE__ */ new Map(),
|
|
@@ -1715,6 +1720,311 @@ function createDirectory() {
|
|
|
1715
1720
|
};
|
|
1716
1721
|
}
|
|
1717
1722
|
|
|
1723
|
+
// src/simulator/beam-direct-charges.ts
|
|
1724
|
+
var SIM_BEAM_HOST = "beam.stardeck.test";
|
|
1725
|
+
var SIM_BEAM_MERCHANT_ID = "sim_merchant";
|
|
1726
|
+
var SIM_BEAM_PUBLISHABLE_KEY = "pk_test_sim";
|
|
1727
|
+
var DIRECT_CHARGE_METHODS = /* @__PURE__ */ new Set([
|
|
1728
|
+
"CARD",
|
|
1729
|
+
"CARD_INSTALLMENTS",
|
|
1730
|
+
"CARD_TOKEN",
|
|
1731
|
+
"CARD_TOKEN_INSTALLMENTS",
|
|
1732
|
+
"QR_PROMPT_PAY",
|
|
1733
|
+
"ALIPAY",
|
|
1734
|
+
"ALIPAY_PLUS",
|
|
1735
|
+
"WECHAT_PAY",
|
|
1736
|
+
"TRUE_MONEY",
|
|
1737
|
+
"LINE_PAY",
|
|
1738
|
+
"SHOPEE_PAY",
|
|
1739
|
+
"KPLUS",
|
|
1740
|
+
"MAKE",
|
|
1741
|
+
"SCB_EASY",
|
|
1742
|
+
"KRUNGSRI_APP",
|
|
1743
|
+
"BANGKOK_BANK_APP",
|
|
1744
|
+
"SPAY_LATER"
|
|
1745
|
+
]);
|
|
1746
|
+
var TEST_CARDS = {
|
|
1747
|
+
"4111111111111111": { outcome: "SUCCEEDED" },
|
|
1748
|
+
"5372074248113841": { outcome: "SUCCEEDED" },
|
|
1749
|
+
"378282246310005": { outcome: "SUCCEEDED" },
|
|
1750
|
+
"4953261730509988": { outcome: "REDIRECT" },
|
|
1751
|
+
"4389783280144506": { outcome: "REDIRECT" },
|
|
1752
|
+
"4943129900084541": { outcome: "FAILED", failureCode: "CH_INSUFFICIENT_FUNDS" },
|
|
1753
|
+
"4111111111000025": { outcome: "FAILED", failureCode: "CH_CARD_DECLINED" },
|
|
1754
|
+
"4111111111000033": { outcome: "FAILED", failureCode: "CH_CARD_RESTRICTED" }
|
|
1755
|
+
};
|
|
1756
|
+
var SIM_QR_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==";
|
|
1757
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
1758
|
+
function invalid(issues) {
|
|
1759
|
+
return json({ error: "Invalid request body", details: issues }, 400);
|
|
1760
|
+
}
|
|
1761
|
+
var DEVICE_TYPES = /* @__PURE__ */ new Set(["ANDROID", "IOS", "WEB"]);
|
|
1762
|
+
function issue(path, message, code = "custom") {
|
|
1763
|
+
return { path, message, code };
|
|
1764
|
+
}
|
|
1765
|
+
function validateChargeFields(body) {
|
|
1766
|
+
const issues = [];
|
|
1767
|
+
const isInt = (v) => typeof v === "number" && Number.isInteger(v);
|
|
1768
|
+
if (!isInt(body.amount) || body.amount < 1) {
|
|
1769
|
+
issues.push(issue(["amount"], "Number must be greater than 0", "too_small"));
|
|
1770
|
+
}
|
|
1771
|
+
if (typeof body.deploymentId !== "string" || !UUID_RE.test(body.deploymentId)) {
|
|
1772
|
+
issues.push(issue(["deploymentId"], "Invalid deployment ID", "invalid_string"));
|
|
1773
|
+
}
|
|
1774
|
+
if (body.referenceId !== void 0 && (typeof body.referenceId !== "string" || body.referenceId.length > 100)) {
|
|
1775
|
+
issues.push(issue(["referenceId"], "Too long", "too_big"));
|
|
1776
|
+
}
|
|
1777
|
+
const expiry = body.expiryDurationInSec;
|
|
1778
|
+
if (expiry !== void 0 && (!isInt(expiry) || expiry < 60 || expiry > 3600)) {
|
|
1779
|
+
issues.push(issue(["expiryDurationInSec"], "Must be an integer from 60 to 3600", "too_big"));
|
|
1780
|
+
}
|
|
1781
|
+
if (body.returnUrl !== void 0 && (typeof body.returnUrl !== "string" || !URL.canParse(body.returnUrl))) {
|
|
1782
|
+
issues.push(issue(["returnUrl"], "Invalid URL", "invalid_format"));
|
|
1783
|
+
}
|
|
1784
|
+
if (body.deviceType !== void 0 && !DEVICE_TYPES.has(String(body.deviceType))) {
|
|
1785
|
+
issues.push(issue(["deviceType"], "Invalid enum value", "invalid_value"));
|
|
1786
|
+
}
|
|
1787
|
+
const key = body.idempotencyKey;
|
|
1788
|
+
if (key !== void 0 && (typeof key !== "string" || key.length < 1 || key.length > 255)) {
|
|
1789
|
+
issues.push(issue(["idempotencyKey"], "Must be 1 to 255 characters", "too_small"));
|
|
1790
|
+
}
|
|
1791
|
+
return issues;
|
|
1792
|
+
}
|
|
1793
|
+
function validateChargeMethod(body) {
|
|
1794
|
+
const method = body.paymentMethod;
|
|
1795
|
+
const type = method?.paymentMethodType;
|
|
1796
|
+
if (typeof type !== "string" || !DIRECT_CHARGE_METHODS.has(type)) {
|
|
1797
|
+
return [issue(["paymentMethod", "paymentMethodType"], "Invalid enum value", "invalid_value")];
|
|
1798
|
+
}
|
|
1799
|
+
const issues = [];
|
|
1800
|
+
const isToken = type === "CARD_TOKEN" || type === "CARD_TOKEN_INSTALLMENTS";
|
|
1801
|
+
const token = method?.cardToken;
|
|
1802
|
+
if (isToken && (typeof token?.cardTokenId !== "string" || token.cardTokenId === "")) {
|
|
1803
|
+
issues.push(
|
|
1804
|
+
issue(["paymentMethod", "cardToken"], `${type} requires paymentMethod.cardToken.cardTokenId`)
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1807
|
+
if (token?.securityCode !== void 0 && !/^\d{3,4}$/.test(String(token.securityCode))) {
|
|
1808
|
+
issues.push(
|
|
1809
|
+
issue(
|
|
1810
|
+
["paymentMethod", "cardToken", "securityCode"],
|
|
1811
|
+
"Invalid security code",
|
|
1812
|
+
"invalid_format"
|
|
1813
|
+
)
|
|
1814
|
+
);
|
|
1815
|
+
}
|
|
1816
|
+
if (type === "CARD_TOKEN_INSTALLMENTS" && ![3, 4, 6, 10].includes(method?.installmentPeriod)) {
|
|
1817
|
+
issues.push(
|
|
1818
|
+
issue(
|
|
1819
|
+
["paymentMethod", "installmentPeriod"],
|
|
1820
|
+
"CARD_TOKEN_INSTALLMENTS requires paymentMethod.installmentPeriod (3, 4, 6 or 10)"
|
|
1821
|
+
)
|
|
1822
|
+
);
|
|
1823
|
+
}
|
|
1824
|
+
if (type !== "QR_PROMPT_PAY" && !body.returnUrl) {
|
|
1825
|
+
issues.push(
|
|
1826
|
+
issue(
|
|
1827
|
+
["returnUrl"],
|
|
1828
|
+
`${type} sends the shopper away to pay; returnUrl is required to bring them back`
|
|
1829
|
+
)
|
|
1830
|
+
);
|
|
1831
|
+
}
|
|
1832
|
+
if (type === "CARD" || type === "CARD_INSTALLMENTS") {
|
|
1833
|
+
issues.push(
|
|
1834
|
+
issue(
|
|
1835
|
+
["paymentMethod", "paymentMethodType"],
|
|
1836
|
+
`${type} takes raw card data; tokenize the card in the browser and charge CARD_TOKEN instead`
|
|
1837
|
+
)
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
return issues;
|
|
1841
|
+
}
|
|
1842
|
+
function validateDirectCharge(body) {
|
|
1843
|
+
return [...validateChargeFields(body), ...validateChargeMethod(body)];
|
|
1844
|
+
}
|
|
1845
|
+
function chargeResponse(charge) {
|
|
1846
|
+
const response = {
|
|
1847
|
+
chargeId: charge.id,
|
|
1848
|
+
actionRequired: charge.actionRequired,
|
|
1849
|
+
paymentMethodType: charge.paymentMethodType
|
|
1850
|
+
};
|
|
1851
|
+
if (charge.actionRequired === "ENCODED_IMAGE") {
|
|
1852
|
+
response.encodedImage = {
|
|
1853
|
+
imageBase64Encoded: SIM_QR_PNG,
|
|
1854
|
+
rawData: `00020101021230SIM${charge.id}`,
|
|
1855
|
+
expiry: charge.expiresAt
|
|
1856
|
+
};
|
|
1857
|
+
}
|
|
1858
|
+
if (charge.actionRequired === "REDIRECT") {
|
|
1859
|
+
response.redirect = { redirectUrl: charge.redirectUrl };
|
|
1860
|
+
response.redirectUrl = charge.redirectUrl;
|
|
1861
|
+
}
|
|
1862
|
+
return response;
|
|
1863
|
+
}
|
|
1864
|
+
function nextChargeId() {
|
|
1865
|
+
state.directChargeCounter += 1;
|
|
1866
|
+
return `ch_sim_${state.directChargeCounter}`;
|
|
1867
|
+
}
|
|
1868
|
+
function createCharge(body) {
|
|
1869
|
+
const method = body.paymentMethod;
|
|
1870
|
+
const type = method.paymentMethodType;
|
|
1871
|
+
const id = nextChargeId();
|
|
1872
|
+
const now3 = /* @__PURE__ */ new Date();
|
|
1873
|
+
const expirySec = typeof body.expiryDurationInSec === "number" ? body.expiryDurationInSec : 30 * 60;
|
|
1874
|
+
const returnUrl = typeof body.returnUrl === "string" ? body.returnUrl : null;
|
|
1875
|
+
let actionRequired = "REDIRECT";
|
|
1876
|
+
let status = "PENDING";
|
|
1877
|
+
let failureCode = null;
|
|
1878
|
+
let cardLast4 = null;
|
|
1879
|
+
if (type === "QR_PROMPT_PAY") {
|
|
1880
|
+
actionRequired = "ENCODED_IMAGE";
|
|
1881
|
+
} else if (type === "CARD_TOKEN" || type === "CARD_TOKEN_INSTALLMENTS") {
|
|
1882
|
+
const token = state.cardTokens.get(method.cardToken.cardTokenId);
|
|
1883
|
+
if (!token) {
|
|
1884
|
+
return json({ error: "Card token not found", code: "CARD_TOKEN_NOT_FOUND" }, 404);
|
|
1885
|
+
}
|
|
1886
|
+
cardLast4 = token.last4;
|
|
1887
|
+
const card = TEST_CARDS[token.pan] ?? {
|
|
1888
|
+
outcome: "FAILED",
|
|
1889
|
+
failureCode: "CH_PROCESSING_FAILED"
|
|
1890
|
+
};
|
|
1891
|
+
if (card.outcome === "REDIRECT") {
|
|
1892
|
+
actionRequired = "REDIRECT";
|
|
1893
|
+
} else {
|
|
1894
|
+
actionRequired = "NONE";
|
|
1895
|
+
status = card.outcome;
|
|
1896
|
+
failureCode = card.failureCode ?? null;
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
const charge = {
|
|
1900
|
+
id,
|
|
1901
|
+
status,
|
|
1902
|
+
amount: body.amount,
|
|
1903
|
+
currency: String(body.currency ?? "THB"),
|
|
1904
|
+
paymentMethodType: type,
|
|
1905
|
+
referenceId: typeof body.referenceId === "string" ? body.referenceId : null,
|
|
1906
|
+
deviceType: typeof body.deviceType === "string" ? body.deviceType : null,
|
|
1907
|
+
returnUrl,
|
|
1908
|
+
actionRequired,
|
|
1909
|
+
redirectUrl: actionRequired === "REDIRECT" ? `https://${SIM_BEAM_HOST}/pay/${id}${returnUrl ? `?returnUrl=${encodeURIComponent(returnUrl)}` : ""}` : null,
|
|
1910
|
+
expiresAt: new Date(now3.getTime() + expirySec * 1e3).toISOString(),
|
|
1911
|
+
failureCode,
|
|
1912
|
+
cardLast4,
|
|
1913
|
+
idempotencyKey: typeof body.idempotencyKey === "string" ? body.idempotencyKey : null,
|
|
1914
|
+
createdAt: now3.toISOString()
|
|
1915
|
+
};
|
|
1916
|
+
state.directCharges.set(id, charge);
|
|
1917
|
+
return charge;
|
|
1918
|
+
}
|
|
1919
|
+
async function handleDirectChargeRequest(request, pathname) {
|
|
1920
|
+
if (request.method === "POST" && /^\/api\/store\/beam\/[^/]+\/charges$/.test(pathname)) {
|
|
1921
|
+
const body = await readJsonBody(request);
|
|
1922
|
+
const issues = validateDirectCharge(body);
|
|
1923
|
+
if (issues.length > 0) return invalid(issues);
|
|
1924
|
+
const key = typeof body.idempotencyKey === "string" ? body.idempotencyKey : null;
|
|
1925
|
+
if (key) {
|
|
1926
|
+
const existing = [...state.directCharges.values()].find((c) => c.idempotencyKey === key);
|
|
1927
|
+
if (existing) return json(chargeResponse(existing));
|
|
1928
|
+
}
|
|
1929
|
+
const created = createCharge(body);
|
|
1930
|
+
if (created instanceof Response) return created;
|
|
1931
|
+
return json(chargeResponse(created));
|
|
1932
|
+
}
|
|
1933
|
+
const getMatch = pathname.match(/^\/api\/store\/beam\/[^/]+\/charges\/([^/]+)$/);
|
|
1934
|
+
if (request.method === "GET" && getMatch) {
|
|
1935
|
+
const charge = state.directCharges.get(decodeURIComponent(getMatch[1]));
|
|
1936
|
+
if (!charge) return json({ error: "Charge not found", code: "NOT_FOUND" }, 404);
|
|
1937
|
+
return json({ charge: toChargeDetail(charge) });
|
|
1938
|
+
}
|
|
1939
|
+
if (request.method === "GET" && /^\/api\/store\/beam\/[^/]+\/card-tokenization$/.test(pathname)) {
|
|
1940
|
+
if (!state.cardTokenizationEnabled) {
|
|
1941
|
+
return json(
|
|
1942
|
+
{
|
|
1943
|
+
error: "Add the Beam publishable key to this merchant account to take cards in your own checkout",
|
|
1944
|
+
code: "CARD_TOKENIZATION_NOT_CONFIGURED"
|
|
1945
|
+
},
|
|
1946
|
+
409
|
|
1947
|
+
);
|
|
1948
|
+
}
|
|
1949
|
+
return json({
|
|
1950
|
+
merchantId: SIM_BEAM_MERCHANT_ID,
|
|
1951
|
+
publishableKey: SIM_BEAM_PUBLISHABLE_KEY,
|
|
1952
|
+
tokenizeUrl: `https://${SIM_BEAM_HOST}/client/v1/card-tokens`,
|
|
1953
|
+
isSandbox: true
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1956
|
+
return null;
|
|
1957
|
+
}
|
|
1958
|
+
function toChargeDetail(charge) {
|
|
1959
|
+
return {
|
|
1960
|
+
id: charge.id,
|
|
1961
|
+
chargeId: charge.id,
|
|
1962
|
+
status: charge.status,
|
|
1963
|
+
amount: charge.amount,
|
|
1964
|
+
currency: charge.currency,
|
|
1965
|
+
paymentMethodType: charge.paymentMethodType,
|
|
1966
|
+
...charge.referenceId !== null ? { referenceId: charge.referenceId } : {},
|
|
1967
|
+
...charge.failureCode ? { failureCode: charge.failureCode } : {},
|
|
1968
|
+
source: "API",
|
|
1969
|
+
createdAt: charge.createdAt
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1972
|
+
async function handleCardTokenRequest(request, url) {
|
|
1973
|
+
if (request.method !== "POST" || url.pathname !== "/client/v1/card-tokens") {
|
|
1974
|
+
return json({ error: `No Beam simulator for ${request.method} ${url.pathname}` }, 404);
|
|
1975
|
+
}
|
|
1976
|
+
const expected = `Basic ${btoa(`${SIM_BEAM_MERCHANT_ID}:${SIM_BEAM_PUBLISHABLE_KEY}`)}`;
|
|
1977
|
+
if (request.headers.get("Authorization") !== expected) {
|
|
1978
|
+
return json(
|
|
1979
|
+
{
|
|
1980
|
+
message: "invalid credentials",
|
|
1981
|
+
error: { errorCode: "INVALID_CREDENTIALS_ERROR", errorMessage: "invalid credentials" }
|
|
1982
|
+
},
|
|
1983
|
+
401
|
|
1984
|
+
);
|
|
1985
|
+
}
|
|
1986
|
+
const body = await readJsonBody(request);
|
|
1987
|
+
const pan = typeof body.pan === "string" ? body.pan : "";
|
|
1988
|
+
const month = body.expiryMonth;
|
|
1989
|
+
const year = body.expiryYear;
|
|
1990
|
+
if (!/^\d{12,19}$/.test(pan) || typeof month !== "number" || month < 1 || month > 12 || typeof year !== "number" || year < 0 || year > 99) {
|
|
1991
|
+
return json(
|
|
1992
|
+
{
|
|
1993
|
+
message: "inputs are failing validation",
|
|
1994
|
+
error: {
|
|
1995
|
+
errorCode: "API_VALIDATION_ERROR",
|
|
1996
|
+
errorMessage: "inputs are failing validation"
|
|
1997
|
+
}
|
|
1998
|
+
},
|
|
1999
|
+
400
|
|
2000
|
+
);
|
|
2001
|
+
}
|
|
2002
|
+
state.cardTokenCounter += 1;
|
|
2003
|
+
const token = {
|
|
2004
|
+
id: `ctok_sim_${state.cardTokenCounter}`,
|
|
2005
|
+
pan,
|
|
2006
|
+
last4: pan.slice(-4),
|
|
2007
|
+
expiryMonth: month,
|
|
2008
|
+
expiryYear: year
|
|
2009
|
+
};
|
|
2010
|
+
state.cardTokens.set(token.id, token);
|
|
2011
|
+
return json({ id: token.id });
|
|
2012
|
+
}
|
|
2013
|
+
function chargeWebhookPayload(charge) {
|
|
2014
|
+
return {
|
|
2015
|
+
chargeId: charge.id,
|
|
2016
|
+
merchantId: SIM_BEAM_MERCHANT_ID,
|
|
2017
|
+
...charge.referenceId !== null ? { referenceId: charge.referenceId } : {},
|
|
2018
|
+
status: charge.status,
|
|
2019
|
+
currency: charge.currency,
|
|
2020
|
+
amount: charge.amount,
|
|
2021
|
+
source: "API",
|
|
2022
|
+
sourceId: charge.id,
|
|
2023
|
+
...charge.failureCode ? { failureCode: charge.failureCode } : {},
|
|
2024
|
+
paymentMethod: { paymentMethodType: charge.paymentMethodType }
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
|
|
1718
2028
|
// src/simulator/payments.ts
|
|
1719
2029
|
var import_node_crypto6 = __toESM(require("crypto"));
|
|
1720
2030
|
|
|
@@ -1829,7 +2139,7 @@ var BEAM_BOLT_PAYMENT_METHODS = [
|
|
|
1829
2139
|
"SPAY_LATER"
|
|
1830
2140
|
];
|
|
1831
2141
|
var BEAM_BOLT_INTENT_STATUSES = ["PENDING", "PAID", "FAILED", "CANCELED", "EXPIRED"];
|
|
1832
|
-
var
|
|
2142
|
+
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
1833
2143
|
function payErr(error, status = 400, code, details) {
|
|
1834
2144
|
const body = { error };
|
|
1835
2145
|
if (code) body.code = code;
|
|
@@ -1843,6 +2153,12 @@ function paymentOperationForRequest(request, pathname) {
|
|
|
1843
2153
|
if (request.method === "GET" && /^\/api\/store\/beam\/[^/]+\/payment-links\/[^/]+$/.test(pathname)) {
|
|
1844
2154
|
return "beam.getPaymentLinkStatus";
|
|
1845
2155
|
}
|
|
2156
|
+
if (request.method === "POST" && /^\/api\/store\/beam\/[^/]+\/charges$/.test(pathname)) {
|
|
2157
|
+
return "beam.createDirectCharge";
|
|
2158
|
+
}
|
|
2159
|
+
if (request.method === "GET" && /^\/api\/store\/beam\/[^/]+\/charges\/[^/]+$/.test(pathname)) {
|
|
2160
|
+
return "beam.getCharge";
|
|
2161
|
+
}
|
|
1846
2162
|
if (request.method === "POST" && /^\/api\/store\/(?!beam\/)[^/]+\/checkout$/.test(pathname)) {
|
|
1847
2163
|
return "stripe.createCheckout";
|
|
1848
2164
|
}
|
|
@@ -2018,6 +2334,8 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2018
2334
|
const pathname = url.pathname;
|
|
2019
2335
|
const injectedFailure = consumePaymentFailure(request, pathname);
|
|
2020
2336
|
if (injectedFailure) return injectedFailure;
|
|
2337
|
+
const directChargeResponse = await handleDirectChargeRequest(request, pathname);
|
|
2338
|
+
if (directChargeResponse) return directChargeResponse;
|
|
2021
2339
|
if (/\/billing-portal$/.test(pathname)) {
|
|
2022
2340
|
return payErr("Not found", 404, "NOT_FOUND");
|
|
2023
2341
|
}
|
|
@@ -2114,7 +2432,7 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2114
2432
|
code: "too_small"
|
|
2115
2433
|
});
|
|
2116
2434
|
}
|
|
2117
|
-
if (typeof body.deploymentId !== "string" || !
|
|
2435
|
+
if (typeof body.deploymentId !== "string" || !UUID_RE2.test(body.deploymentId)) {
|
|
2118
2436
|
issues.push({
|
|
2119
2437
|
path: ["deploymentId"],
|
|
2120
2438
|
message: "Invalid uuid",
|
|
@@ -2301,6 +2619,18 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2301
2619
|
}
|
|
2302
2620
|
return payErr(`No payments simulator for ${request.method} ${pathname}`, 404);
|
|
2303
2621
|
}
|
|
2622
|
+
function requirePendingDirectCharge(chargeId) {
|
|
2623
|
+
const charge = state.directCharges.get(chargeId);
|
|
2624
|
+
if (!charge) {
|
|
2625
|
+
throw new Error(`[stardeck-testing] Unknown direct charge id: ${chargeId}`);
|
|
2626
|
+
}
|
|
2627
|
+
if (charge.status !== "PENDING") {
|
|
2628
|
+
throw new Error(
|
|
2629
|
+
`[stardeck-testing] Direct charge ${chargeId} is ${charge.status}, not PENDING`
|
|
2630
|
+
);
|
|
2631
|
+
}
|
|
2632
|
+
return charge;
|
|
2633
|
+
}
|
|
2304
2634
|
function createPayments() {
|
|
2305
2635
|
return {
|
|
2306
2636
|
get checkouts() {
|
|
@@ -2419,6 +2749,26 @@ function createPayments() {
|
|
|
2419
2749
|
if (!intent) return void 0;
|
|
2420
2750
|
return toBoltIntentRecord(intent);
|
|
2421
2751
|
},
|
|
2752
|
+
get directCharges() {
|
|
2753
|
+
return [...state.directCharges.values()];
|
|
2754
|
+
},
|
|
2755
|
+
getDirectCharge(chargeId) {
|
|
2756
|
+
return state.directCharges.get(chargeId);
|
|
2757
|
+
},
|
|
2758
|
+
completeDirectCharge(chargeId) {
|
|
2759
|
+
const charge = requirePendingDirectCharge(chargeId);
|
|
2760
|
+
charge.status = "SUCCEEDED";
|
|
2761
|
+
return chargeWebhookPayload(charge);
|
|
2762
|
+
},
|
|
2763
|
+
failDirectCharge(chargeId, failureCode = "CH_PROCESSING_FAILED") {
|
|
2764
|
+
const charge = requirePendingDirectCharge(chargeId);
|
|
2765
|
+
charge.status = "FAILED";
|
|
2766
|
+
charge.failureCode = failureCode;
|
|
2767
|
+
return chargeWebhookPayload(charge);
|
|
2768
|
+
},
|
|
2769
|
+
setCardTokenization(enabled) {
|
|
2770
|
+
state.cardTokenizationEnabled = enabled;
|
|
2771
|
+
},
|
|
2422
2772
|
clear() {
|
|
2423
2773
|
state.checkouts = [];
|
|
2424
2774
|
state.checkoutCounter = 0;
|
|
@@ -2433,6 +2783,11 @@ function createPayments() {
|
|
|
2433
2783
|
state.boltIntentCounter = 0;
|
|
2434
2784
|
state.boltCharges = [];
|
|
2435
2785
|
state.boltChargeCounter = 0;
|
|
2786
|
+
state.directCharges.clear();
|
|
2787
|
+
state.directChargeCounter = 0;
|
|
2788
|
+
state.cardTokens.clear();
|
|
2789
|
+
state.cardTokenCounter = 0;
|
|
2790
|
+
state.cardTokenizationEnabled = true;
|
|
2436
2791
|
},
|
|
2437
2792
|
get count() {
|
|
2438
2793
|
return state.checkouts.length;
|
|
@@ -3312,6 +3667,9 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3312
3667
|
if (isStorageHost) {
|
|
3313
3668
|
return handleStorageRequest(request, url);
|
|
3314
3669
|
}
|
|
3670
|
+
if (url.hostname === SIM_BEAM_HOST) {
|
|
3671
|
+
return handleCardTokenRequest(request, url);
|
|
3672
|
+
}
|
|
3315
3673
|
if (isEmail && request.method === "POST") {
|
|
3316
3674
|
return handleEmailSend(request);
|
|
3317
3675
|
}
|