@stardeck-customer-apps/testing 0.8.0 → 0.9.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/dist/index.js CHANGED
@@ -105,6 +105,13 @@ var state = globalSingleton("state", () => ({
105
105
  sessionStatuses: /* @__PURE__ */ new Map(),
106
106
  paymentLinks: /* @__PURE__ */ new Map(),
107
107
  products: [],
108
+ boltConnections: /* @__PURE__ */ new Map(),
109
+ boltUsedPairingCodes: /* @__PURE__ */ new Set(),
110
+ boltConnectionCounter: 0,
111
+ boltIntents: /* @__PURE__ */ new Map(),
112
+ boltIntentCounter: 0,
113
+ boltCharges: [],
114
+ boltChargeCounter: 0,
108
115
  uploads: [],
109
116
  uploadCounter: 0,
110
117
  storageFiles: /* @__PURE__ */ new Map(),
@@ -1242,8 +1249,31 @@ function signEventDelivery(secret, context, rawBody) {
1242
1249
  }
1243
1250
 
1244
1251
  // src/simulator/payments.ts
1245
- function payErr(error, status = 400, code) {
1246
- return json(code ? { error, code } : { error }, status);
1252
+ var BEAM_BOLT_PAYMENT_METHODS = [
1253
+ "CARD",
1254
+ "CARD_INSTALLMENTS",
1255
+ "QR_PROMPT_PAY",
1256
+ "ALIPAY",
1257
+ "ALIPAY_PLUS",
1258
+ "LINE_PAY",
1259
+ "SHOPEE_PAY",
1260
+ "TRUE_MONEY",
1261
+ "WECHAT_PAY",
1262
+ "SPAY_LATER"
1263
+ ];
1264
+ var BEAM_BOLT_INTENT_STATUSES = ["PENDING", "PAID", "FAILED", "CANCELED", "EXPIRED"];
1265
+ 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;
1266
+ function payErr(error, status = 400, code, details) {
1267
+ const body = { error };
1268
+ if (code) body.code = code;
1269
+ if (details !== void 0) body.details = details;
1270
+ return json(body, status);
1271
+ }
1272
+ function isBoltPaymentMethod(value) {
1273
+ return typeof value === "string" && BEAM_BOLT_PAYMENT_METHODS.includes(value);
1274
+ }
1275
+ function isBoltIntentStatus(value) {
1276
+ return BEAM_BOLT_INTENT_STATUSES.includes(value);
1247
1277
  }
1248
1278
  function nextCheckoutId() {
1249
1279
  state.checkoutCounter += 1;
@@ -1253,6 +1283,18 @@ function nextPaymentLinkId() {
1253
1283
  state.checkoutCounter += 1;
1254
1284
  return `plink_test_${state.checkoutCounter}`;
1255
1285
  }
1286
+ function nextBoltConnectionId() {
1287
+ state.boltConnectionCounter += 1;
1288
+ return `boltc_${state.boltConnectionCounter}`;
1289
+ }
1290
+ function nextBoltIntentId() {
1291
+ state.boltIntentCounter += 1;
1292
+ return `bolti_${state.boltIntentCounter}`;
1293
+ }
1294
+ function nextBoltChargeId() {
1295
+ state.boltChargeCounter += 1;
1296
+ return `chrg_${state.boltChargeCounter}`;
1297
+ }
1256
1298
  function seedStripeSession(id, body) {
1257
1299
  const lineItems = body.lineItems ?? [];
1258
1300
  let amountTotal = null;
@@ -1296,6 +1338,58 @@ function seedBeamLink(id, body, merchantId) {
1296
1338
  collectDeliveryAddress: body.collectDeliveryAddress === true
1297
1339
  });
1298
1340
  }
1341
+ function deriveBoltIntentStatus(storedStatus, expiresAt, now3 = /* @__PURE__ */ new Date()) {
1342
+ if (storedStatus !== "PENDING") return storedStatus;
1343
+ return expiresAt.getTime() <= now3.getTime() ? "EXPIRED" : "PENDING";
1344
+ }
1345
+ function toBoltIntentRecord(intent, now3 = /* @__PURE__ */ new Date()) {
1346
+ return {
1347
+ id: intent.id,
1348
+ beamIntentId: intent.beamIntentId,
1349
+ boltConnectionId: intent.boltConnectionId,
1350
+ amount: intent.amount,
1351
+ currency: intent.currency,
1352
+ paymentMethodType: intent.paymentMethodType,
1353
+ referenceId: intent.referenceId,
1354
+ internalNote: intent.internalNote,
1355
+ status: deriveBoltIntentStatus(intent.status, intent.expiresAt, now3),
1356
+ isVirtual: intent.isVirtual,
1357
+ environment: intent.environment,
1358
+ expiresAt: intent.expiresAt.toISOString(),
1359
+ settledAt: intent.settledAt ? intent.settledAt.toISOString() : null,
1360
+ chargeId: intent.chargeId,
1361
+ failureReason: intent.failureReason,
1362
+ createdAt: intent.createdAt.toISOString()
1363
+ };
1364
+ }
1365
+ function findBoltConnection(connectionId) {
1366
+ return state.boltConnections.get(connectionId) ?? [...state.boltConnections.values()].find((c) => c.beamConnectionId === connectionId);
1367
+ }
1368
+ function findBoltIntent(intentId) {
1369
+ return state.boltIntents.get(intentId) ?? [...state.boltIntents.values()].find((i) => i.beamIntentId === intentId);
1370
+ }
1371
+ function requirePendingBoltIntent(intentId) {
1372
+ const intent = findBoltIntent(intentId);
1373
+ if (!intent) {
1374
+ throw new Error(`[stardeck-testing] Unknown bolt intent id: ${intentId}`);
1375
+ }
1376
+ const derived = deriveBoltIntentStatus(intent.status, intent.expiresAt);
1377
+ if (derived !== "PENDING") {
1378
+ throw new Error(`[stardeck-testing] Bolt intent ${intentId} is ${derived}, expected PENDING`);
1379
+ }
1380
+ return intent;
1381
+ }
1382
+ function storedStatusesForFilter(statuses) {
1383
+ const stored = /* @__PURE__ */ new Set();
1384
+ for (const status of statuses) {
1385
+ if (status === "EXPIRED" || status === "PENDING") {
1386
+ stored.add("PENDING");
1387
+ } else {
1388
+ stored.add(status);
1389
+ }
1390
+ }
1391
+ return [...stored];
1392
+ }
1299
1393
  async function deliverEvent(envelope, handler, options) {
1300
1394
  const rawBody = JSON.stringify(envelope);
1301
1395
  const secret = options?.deploymentSecret ?? TEST_ENV_DEFAULTS.DEPLOYMENT_SECRET;
@@ -1318,9 +1412,219 @@ async function deliverEvent(envelope, handler, options) {
1318
1412
  }
1319
1413
  async function handlePaymentsRequest(request, url) {
1320
1414
  const pathname = url.pathname;
1321
- if (/\/bolt-connections/.test(pathname) || /\/bolt-intents/.test(pathname) || /\/charges(\/|$)/.test(pathname) || /\/billing-portal$/.test(pathname)) {
1415
+ if (/\/billing-portal$/.test(pathname)) {
1322
1416
  return payErr("Not found", 404, "NOT_FOUND");
1323
1417
  }
1418
+ const boltConnectionsMatch = pathname.match(
1419
+ /^\/api\/store\/beam\/([^/]+)\/bolt-connections(?:\/([^/]+))?$/
1420
+ );
1421
+ if (boltConnectionsMatch) {
1422
+ const connectionId = boltConnectionsMatch[2];
1423
+ if (!connectionId && request.method === "POST") {
1424
+ const body = await readJsonBody(request);
1425
+ const pairingCode = body.pairingCode ? String(body.pairingCode) : "";
1426
+ if (!pairingCode) {
1427
+ return payErr("pairingCode is required", 400);
1428
+ }
1429
+ if (state.boltUsedPairingCodes.has(pairingCode)) {
1430
+ return payErr("Pairing code has already been used", 400, "PAIRING_CODE_USED");
1431
+ }
1432
+ const id = nextBoltConnectionId();
1433
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
1434
+ const environments = Array.isArray(body.environments) ? body.environments.map(String) : ["sandbox"];
1435
+ const isSandbox = environments.some((e) => e === "sandbox" || e === "preview");
1436
+ const connection = {
1437
+ id,
1438
+ projectId: TEST_ENV_DEFAULTS.PROJECT_ID,
1439
+ beamConnectionId: id,
1440
+ displayName: body.displayName != null ? String(body.displayName) : null,
1441
+ pairingCode,
1442
+ status: "ACTIVE",
1443
+ isSandbox,
1444
+ environments,
1445
+ createdAt: now3,
1446
+ updatedAt: now3
1447
+ };
1448
+ state.boltUsedPairingCodes.add(pairingCode);
1449
+ state.boltConnections.set(id, connection);
1450
+ return json({ connection });
1451
+ }
1452
+ if (!connectionId && request.method === "GET") {
1453
+ return json({ connections: [...state.boltConnections.values()] });
1454
+ }
1455
+ if (connectionId && request.method === "GET") {
1456
+ const connection = findBoltConnection(connectionId);
1457
+ if (!connection) return payErr("Bolt connection not found", 404, "NOT_FOUND");
1458
+ return json({ connection });
1459
+ }
1460
+ if (connectionId && request.method === "DELETE") {
1461
+ const connection = findBoltConnection(connectionId);
1462
+ if (!connection) return payErr("Bolt connection not found", 404, "NOT_FOUND");
1463
+ state.boltConnections.delete(connection.id);
1464
+ return json({ success: true });
1465
+ }
1466
+ }
1467
+ const boltIntentsMatch = pathname.match(
1468
+ /^\/api\/store\/beam\/([^/]+)\/bolt-intents(?:\/([^/]+))?$/
1469
+ );
1470
+ if (boltIntentsMatch) {
1471
+ const intentId = boltIntentsMatch[2];
1472
+ if (!intentId && request.method === "POST") {
1473
+ const body = await readJsonBody(request);
1474
+ const issues = [];
1475
+ if (typeof body.amount !== "number" || !Number.isInteger(body.amount) || body.amount < 1) {
1476
+ issues.push({
1477
+ path: ["amount"],
1478
+ message: "Number must be greater than 0",
1479
+ code: "too_small"
1480
+ });
1481
+ }
1482
+ const boltConnectionIdRaw = body.boltConnectionId;
1483
+ if (typeof boltConnectionIdRaw !== "string" || boltConnectionIdRaw.length < 1) {
1484
+ issues.push({
1485
+ path: ["boltConnectionId"],
1486
+ message: "String must contain at least 1 character(s)",
1487
+ code: "too_small"
1488
+ });
1489
+ }
1490
+ const paymentMethod = body.paymentMethod;
1491
+ if (!paymentMethod || typeof paymentMethod !== "object") {
1492
+ issues.push({
1493
+ path: ["paymentMethod"],
1494
+ message: "Required",
1495
+ code: "invalid_type"
1496
+ });
1497
+ } else if (!isBoltPaymentMethod(paymentMethod.paymentMethodType)) {
1498
+ issues.push({
1499
+ path: ["paymentMethod", "paymentMethodType"],
1500
+ message: "Invalid enum value",
1501
+ code: "invalid_enum_value"
1502
+ });
1503
+ }
1504
+ if (typeof body.expiryDurationInSec !== "number" || !Number.isInteger(body.expiryDurationInSec) || body.expiryDurationInSec < 90 || body.expiryDurationInSec > 600) {
1505
+ issues.push({
1506
+ path: ["expiryDurationInSec"],
1507
+ message: "Number must be between 90 and 600",
1508
+ code: "too_small"
1509
+ });
1510
+ }
1511
+ if (typeof body.deploymentId !== "string" || !UUID_RE.test(body.deploymentId)) {
1512
+ issues.push({
1513
+ path: ["deploymentId"],
1514
+ message: "Invalid uuid",
1515
+ code: "invalid_string"
1516
+ });
1517
+ }
1518
+ if (issues.length > 0) {
1519
+ return payErr("Invalid request body", 400, void 0, issues);
1520
+ }
1521
+ const boltConnectionId = String(body.boltConnectionId);
1522
+ if (!findBoltConnection(boltConnectionId)) {
1523
+ return payErr("Bolt connection not found", 404, "NOT_FOUND");
1524
+ }
1525
+ const paymentMethodType = body.paymentMethod.paymentMethodType;
1526
+ const amount = body.amount;
1527
+ const currency = String(body.currency ?? "THB");
1528
+ const expiryDurationInSec = body.expiryDurationInSec;
1529
+ const id = nextBoltIntentId();
1530
+ const now3 = /* @__PURE__ */ new Date();
1531
+ const intent = {
1532
+ id,
1533
+ beamIntentId: id,
1534
+ boltConnectionId,
1535
+ amount,
1536
+ currency,
1537
+ paymentMethodType,
1538
+ referenceId: body.referenceId != null ? String(body.referenceId) : null,
1539
+ internalNote: body.internalNote != null ? String(body.internalNote) : null,
1540
+ status: "PENDING",
1541
+ isVirtual: false,
1542
+ environment: "sandbox",
1543
+ expiresAt: new Date(now3.getTime() + expiryDurationInSec * 1e3),
1544
+ settledAt: null,
1545
+ chargeId: null,
1546
+ failureReason: null,
1547
+ createdAt: now3
1548
+ };
1549
+ state.boltIntents.set(id, intent);
1550
+ return json({
1551
+ id,
1552
+ status: "PENDING",
1553
+ amount,
1554
+ currency,
1555
+ createdAt: now3.toISOString()
1556
+ });
1557
+ }
1558
+ if (!intentId && request.method === "GET") {
1559
+ const deploymentId = url.searchParams.get("deploymentId");
1560
+ if (!deploymentId) {
1561
+ return payErr("Missing deploymentId parameter", 400);
1562
+ }
1563
+ const now3 = /* @__PURE__ */ new Date();
1564
+ const statusParam = url.searchParams.get("status");
1565
+ let requestedStatuses;
1566
+ if (statusParam) {
1567
+ const parts = statusParam.split(",").map((s) => s.trim()).filter(Boolean);
1568
+ const parsed = [];
1569
+ for (const part of parts) {
1570
+ if (!isBoltIntentStatus(part)) {
1571
+ return payErr(`Invalid status: ${part}`, 400);
1572
+ }
1573
+ parsed.push(part);
1574
+ }
1575
+ requestedStatuses = parsed;
1576
+ }
1577
+ const boltConnectionId = url.searchParams.get("boltConnectionId") ?? void 0;
1578
+ const limitParam = url.searchParams.get("limit");
1579
+ let limit = 50;
1580
+ if (limitParam !== null) {
1581
+ const trimmed = limitParam.trim();
1582
+ if (!/^\d+$/.test(trimmed)) {
1583
+ return payErr("limit must be an integer between 1 and 100", 400);
1584
+ }
1585
+ const parsed = Number.parseInt(trimmed, 10);
1586
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) {
1587
+ return payErr("limit must be an integer between 1 and 100", 400);
1588
+ }
1589
+ limit = parsed;
1590
+ }
1591
+ let intents = [...state.boltIntents.values()];
1592
+ if (boltConnectionId) {
1593
+ intents = intents.filter((i) => i.boltConnectionId === boltConnectionId);
1594
+ }
1595
+ if (requestedStatuses && requestedStatuses.length > 0) {
1596
+ const storedWanted = new Set(storedStatusesForFilter(requestedStatuses));
1597
+ intents = intents.filter((i) => storedWanted.has(i.status));
1598
+ }
1599
+ let records = intents.map((i) => toBoltIntentRecord(i, now3));
1600
+ if (requestedStatuses && requestedStatuses.length > 0) {
1601
+ const wanted = new Set(requestedStatuses);
1602
+ records = records.filter((r) => wanted.has(r.status));
1603
+ }
1604
+ records = records.slice(0, limit);
1605
+ return json({ intents: records });
1606
+ }
1607
+ if (intentId && request.method === "DELETE") {
1608
+ const intent = findBoltIntent(intentId);
1609
+ if (!intent) return payErr("Bolt intent not found", 404, "NOT_FOUND");
1610
+ const derived = deriveBoltIntentStatus(intent.status, intent.expiresAt);
1611
+ if (derived !== "PENDING") {
1612
+ return payErr(`Bolt intent is ${derived}, only PENDING intents can be canceled`, 409);
1613
+ }
1614
+ intent.status = "CANCELED";
1615
+ intent.settledAt = /* @__PURE__ */ new Date();
1616
+ return json({ success: true });
1617
+ }
1618
+ }
1619
+ const chargesMatch = pathname.match(/^\/api\/store\/beam\/([^/]+)\/charges$/);
1620
+ if (chargesMatch && request.method === "GET") {
1621
+ const sourceId = url.searchParams.get("sourceId");
1622
+ if (!sourceId) {
1623
+ return payErr("Missing sourceId parameter", 400);
1624
+ }
1625
+ const charges = state.boltCharges.filter((c) => c.sourceId === sourceId);
1626
+ return json({ charges });
1627
+ }
1324
1628
  const beamProductsMatch = pathname.match(
1325
1629
  /^\/api\/store\/beam\/([^/]+)\/payment-links(?:\/([^/]+))?$/
1326
1630
  );
@@ -1455,12 +1759,70 @@ function createPayments() {
1455
1759
  };
1456
1760
  return deliverEvent(envelope, handler, options);
1457
1761
  },
1762
+ approveBoltIntent(intentId, options) {
1763
+ const intent = requirePendingBoltIntent(intentId);
1764
+ const chargeId = options?.chargeId ?? nextBoltChargeId();
1765
+ const charge = {
1766
+ id: chargeId,
1767
+ status: "SUCCEEDED",
1768
+ amount: intent.amount,
1769
+ currency: intent.currency,
1770
+ sourceId: intent.beamIntentId,
1771
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1772
+ };
1773
+ intent.status = "PAID";
1774
+ intent.chargeId = chargeId;
1775
+ intent.settledAt = /* @__PURE__ */ new Date();
1776
+ state.boltCharges.push(charge);
1777
+ return charge;
1778
+ },
1779
+ // No webhook on decline: Beam has no charge-failure event for bolt payments.
1780
+ declineBoltIntent(intentId, options) {
1781
+ const intent = requirePendingBoltIntent(intentId);
1782
+ const chargeId = nextBoltChargeId();
1783
+ const failureReason = options?.failureReason ?? "declined";
1784
+ const charge = {
1785
+ id: chargeId,
1786
+ status: "FAILED",
1787
+ amount: intent.amount,
1788
+ currency: intent.currency,
1789
+ sourceId: intent.beamIntentId,
1790
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1791
+ };
1792
+ intent.status = "FAILED";
1793
+ intent.chargeId = chargeId;
1794
+ intent.failureReason = failureReason;
1795
+ intent.settledAt = /* @__PURE__ */ new Date();
1796
+ state.boltCharges.push(charge);
1797
+ return charge;
1798
+ },
1799
+ // No webhook on expire: Beam does not notify apps when a bolt intent expires.
1800
+ expireBoltIntent(intentId) {
1801
+ const intent = requirePendingBoltIntent(intentId);
1802
+ intent.expiresAt = new Date(Date.now() - 1e3);
1803
+ },
1804
+ listBoltIntents() {
1805
+ const now3 = /* @__PURE__ */ new Date();
1806
+ return [...state.boltIntents.values()].map((i) => toBoltIntentRecord(i, now3));
1807
+ },
1808
+ getBoltIntent(intentId) {
1809
+ const intent = findBoltIntent(intentId);
1810
+ if (!intent) return void 0;
1811
+ return toBoltIntentRecord(intent);
1812
+ },
1458
1813
  clear() {
1459
1814
  state.checkouts = [];
1460
1815
  state.checkoutCounter = 0;
1461
1816
  state.sessionStatuses.clear();
1462
1817
  state.paymentLinks.clear();
1463
1818
  state.products = [];
1819
+ state.boltConnections.clear();
1820
+ state.boltUsedPairingCodes.clear();
1821
+ state.boltConnectionCounter = 0;
1822
+ state.boltIntents.clear();
1823
+ state.boltIntentCounter = 0;
1824
+ state.boltCharges = [];
1825
+ state.boltChargeCounter = 0;
1464
1826
  },
1465
1827
  get count() {
1466
1828
  return state.checkouts.length;