@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.mjs CHANGED
@@ -59,6 +59,13 @@ var state = globalSingleton("state", () => ({
59
59
  sessionStatuses: /* @__PURE__ */ new Map(),
60
60
  paymentLinks: /* @__PURE__ */ new Map(),
61
61
  products: [],
62
+ boltConnections: /* @__PURE__ */ new Map(),
63
+ boltUsedPairingCodes: /* @__PURE__ */ new Set(),
64
+ boltConnectionCounter: 0,
65
+ boltIntents: /* @__PURE__ */ new Map(),
66
+ boltIntentCounter: 0,
67
+ boltCharges: [],
68
+ boltChargeCounter: 0,
62
69
  uploads: [],
63
70
  uploadCounter: 0,
64
71
  storageFiles: /* @__PURE__ */ new Map(),
@@ -1196,8 +1203,31 @@ function signEventDelivery(secret, context, rawBody) {
1196
1203
  }
1197
1204
 
1198
1205
  // src/simulator/payments.ts
1199
- function payErr(error, status = 400, code) {
1200
- return json(code ? { error, code } : { error }, status);
1206
+ var BEAM_BOLT_PAYMENT_METHODS = [
1207
+ "CARD",
1208
+ "CARD_INSTALLMENTS",
1209
+ "QR_PROMPT_PAY",
1210
+ "ALIPAY",
1211
+ "ALIPAY_PLUS",
1212
+ "LINE_PAY",
1213
+ "SHOPEE_PAY",
1214
+ "TRUE_MONEY",
1215
+ "WECHAT_PAY",
1216
+ "SPAY_LATER"
1217
+ ];
1218
+ var BEAM_BOLT_INTENT_STATUSES = ["PENDING", "PAID", "FAILED", "CANCELED", "EXPIRED"];
1219
+ 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;
1220
+ function payErr(error, status = 400, code, details) {
1221
+ const body = { error };
1222
+ if (code) body.code = code;
1223
+ if (details !== void 0) body.details = details;
1224
+ return json(body, status);
1225
+ }
1226
+ function isBoltPaymentMethod(value) {
1227
+ return typeof value === "string" && BEAM_BOLT_PAYMENT_METHODS.includes(value);
1228
+ }
1229
+ function isBoltIntentStatus(value) {
1230
+ return BEAM_BOLT_INTENT_STATUSES.includes(value);
1201
1231
  }
1202
1232
  function nextCheckoutId() {
1203
1233
  state.checkoutCounter += 1;
@@ -1207,6 +1237,18 @@ function nextPaymentLinkId() {
1207
1237
  state.checkoutCounter += 1;
1208
1238
  return `plink_test_${state.checkoutCounter}`;
1209
1239
  }
1240
+ function nextBoltConnectionId() {
1241
+ state.boltConnectionCounter += 1;
1242
+ return `boltc_${state.boltConnectionCounter}`;
1243
+ }
1244
+ function nextBoltIntentId() {
1245
+ state.boltIntentCounter += 1;
1246
+ return `bolti_${state.boltIntentCounter}`;
1247
+ }
1248
+ function nextBoltChargeId() {
1249
+ state.boltChargeCounter += 1;
1250
+ return `chrg_${state.boltChargeCounter}`;
1251
+ }
1210
1252
  function seedStripeSession(id, body) {
1211
1253
  const lineItems = body.lineItems ?? [];
1212
1254
  let amountTotal = null;
@@ -1250,6 +1292,58 @@ function seedBeamLink(id, body, merchantId) {
1250
1292
  collectDeliveryAddress: body.collectDeliveryAddress === true
1251
1293
  });
1252
1294
  }
1295
+ function deriveBoltIntentStatus(storedStatus, expiresAt, now3 = /* @__PURE__ */ new Date()) {
1296
+ if (storedStatus !== "PENDING") return storedStatus;
1297
+ return expiresAt.getTime() <= now3.getTime() ? "EXPIRED" : "PENDING";
1298
+ }
1299
+ function toBoltIntentRecord(intent, now3 = /* @__PURE__ */ new Date()) {
1300
+ return {
1301
+ id: intent.id,
1302
+ beamIntentId: intent.beamIntentId,
1303
+ boltConnectionId: intent.boltConnectionId,
1304
+ amount: intent.amount,
1305
+ currency: intent.currency,
1306
+ paymentMethodType: intent.paymentMethodType,
1307
+ referenceId: intent.referenceId,
1308
+ internalNote: intent.internalNote,
1309
+ status: deriveBoltIntentStatus(intent.status, intent.expiresAt, now3),
1310
+ isVirtual: intent.isVirtual,
1311
+ environment: intent.environment,
1312
+ expiresAt: intent.expiresAt.toISOString(),
1313
+ settledAt: intent.settledAt ? intent.settledAt.toISOString() : null,
1314
+ chargeId: intent.chargeId,
1315
+ failureReason: intent.failureReason,
1316
+ createdAt: intent.createdAt.toISOString()
1317
+ };
1318
+ }
1319
+ function findBoltConnection(connectionId) {
1320
+ return state.boltConnections.get(connectionId) ?? [...state.boltConnections.values()].find((c) => c.beamConnectionId === connectionId);
1321
+ }
1322
+ function findBoltIntent(intentId) {
1323
+ return state.boltIntents.get(intentId) ?? [...state.boltIntents.values()].find((i) => i.beamIntentId === intentId);
1324
+ }
1325
+ function requirePendingBoltIntent(intentId) {
1326
+ const intent = findBoltIntent(intentId);
1327
+ if (!intent) {
1328
+ throw new Error(`[stardeck-testing] Unknown bolt intent id: ${intentId}`);
1329
+ }
1330
+ const derived = deriveBoltIntentStatus(intent.status, intent.expiresAt);
1331
+ if (derived !== "PENDING") {
1332
+ throw new Error(`[stardeck-testing] Bolt intent ${intentId} is ${derived}, expected PENDING`);
1333
+ }
1334
+ return intent;
1335
+ }
1336
+ function storedStatusesForFilter(statuses) {
1337
+ const stored = /* @__PURE__ */ new Set();
1338
+ for (const status of statuses) {
1339
+ if (status === "EXPIRED" || status === "PENDING") {
1340
+ stored.add("PENDING");
1341
+ } else {
1342
+ stored.add(status);
1343
+ }
1344
+ }
1345
+ return [...stored];
1346
+ }
1253
1347
  async function deliverEvent(envelope, handler, options) {
1254
1348
  const rawBody = JSON.stringify(envelope);
1255
1349
  const secret = options?.deploymentSecret ?? TEST_ENV_DEFAULTS.DEPLOYMENT_SECRET;
@@ -1272,9 +1366,219 @@ async function deliverEvent(envelope, handler, options) {
1272
1366
  }
1273
1367
  async function handlePaymentsRequest(request, url) {
1274
1368
  const pathname = url.pathname;
1275
- if (/\/bolt-connections/.test(pathname) || /\/bolt-intents/.test(pathname) || /\/charges(\/|$)/.test(pathname) || /\/billing-portal$/.test(pathname)) {
1369
+ if (/\/billing-portal$/.test(pathname)) {
1276
1370
  return payErr("Not found", 404, "NOT_FOUND");
1277
1371
  }
1372
+ const boltConnectionsMatch = pathname.match(
1373
+ /^\/api\/store\/beam\/([^/]+)\/bolt-connections(?:\/([^/]+))?$/
1374
+ );
1375
+ if (boltConnectionsMatch) {
1376
+ const connectionId = boltConnectionsMatch[2];
1377
+ if (!connectionId && request.method === "POST") {
1378
+ const body = await readJsonBody(request);
1379
+ const pairingCode = body.pairingCode ? String(body.pairingCode) : "";
1380
+ if (!pairingCode) {
1381
+ return payErr("pairingCode is required", 400);
1382
+ }
1383
+ if (state.boltUsedPairingCodes.has(pairingCode)) {
1384
+ return payErr("Pairing code has already been used", 400, "PAIRING_CODE_USED");
1385
+ }
1386
+ const id = nextBoltConnectionId();
1387
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
1388
+ const environments = Array.isArray(body.environments) ? body.environments.map(String) : ["sandbox"];
1389
+ const isSandbox = environments.some((e) => e === "sandbox" || e === "preview");
1390
+ const connection = {
1391
+ id,
1392
+ projectId: TEST_ENV_DEFAULTS.PROJECT_ID,
1393
+ beamConnectionId: id,
1394
+ displayName: body.displayName != null ? String(body.displayName) : null,
1395
+ pairingCode,
1396
+ status: "ACTIVE",
1397
+ isSandbox,
1398
+ environments,
1399
+ createdAt: now3,
1400
+ updatedAt: now3
1401
+ };
1402
+ state.boltUsedPairingCodes.add(pairingCode);
1403
+ state.boltConnections.set(id, connection);
1404
+ return json({ connection });
1405
+ }
1406
+ if (!connectionId && request.method === "GET") {
1407
+ return json({ connections: [...state.boltConnections.values()] });
1408
+ }
1409
+ if (connectionId && request.method === "GET") {
1410
+ const connection = findBoltConnection(connectionId);
1411
+ if (!connection) return payErr("Bolt connection not found", 404, "NOT_FOUND");
1412
+ return json({ connection });
1413
+ }
1414
+ if (connectionId && request.method === "DELETE") {
1415
+ const connection = findBoltConnection(connectionId);
1416
+ if (!connection) return payErr("Bolt connection not found", 404, "NOT_FOUND");
1417
+ state.boltConnections.delete(connection.id);
1418
+ return json({ success: true });
1419
+ }
1420
+ }
1421
+ const boltIntentsMatch = pathname.match(
1422
+ /^\/api\/store\/beam\/([^/]+)\/bolt-intents(?:\/([^/]+))?$/
1423
+ );
1424
+ if (boltIntentsMatch) {
1425
+ const intentId = boltIntentsMatch[2];
1426
+ if (!intentId && request.method === "POST") {
1427
+ const body = await readJsonBody(request);
1428
+ const issues = [];
1429
+ if (typeof body.amount !== "number" || !Number.isInteger(body.amount) || body.amount < 1) {
1430
+ issues.push({
1431
+ path: ["amount"],
1432
+ message: "Number must be greater than 0",
1433
+ code: "too_small"
1434
+ });
1435
+ }
1436
+ const boltConnectionIdRaw = body.boltConnectionId;
1437
+ if (typeof boltConnectionIdRaw !== "string" || boltConnectionIdRaw.length < 1) {
1438
+ issues.push({
1439
+ path: ["boltConnectionId"],
1440
+ message: "String must contain at least 1 character(s)",
1441
+ code: "too_small"
1442
+ });
1443
+ }
1444
+ const paymentMethod = body.paymentMethod;
1445
+ if (!paymentMethod || typeof paymentMethod !== "object") {
1446
+ issues.push({
1447
+ path: ["paymentMethod"],
1448
+ message: "Required",
1449
+ code: "invalid_type"
1450
+ });
1451
+ } else if (!isBoltPaymentMethod(paymentMethod.paymentMethodType)) {
1452
+ issues.push({
1453
+ path: ["paymentMethod", "paymentMethodType"],
1454
+ message: "Invalid enum value",
1455
+ code: "invalid_enum_value"
1456
+ });
1457
+ }
1458
+ if (typeof body.expiryDurationInSec !== "number" || !Number.isInteger(body.expiryDurationInSec) || body.expiryDurationInSec < 90 || body.expiryDurationInSec > 600) {
1459
+ issues.push({
1460
+ path: ["expiryDurationInSec"],
1461
+ message: "Number must be between 90 and 600",
1462
+ code: "too_small"
1463
+ });
1464
+ }
1465
+ if (typeof body.deploymentId !== "string" || !UUID_RE.test(body.deploymentId)) {
1466
+ issues.push({
1467
+ path: ["deploymentId"],
1468
+ message: "Invalid uuid",
1469
+ code: "invalid_string"
1470
+ });
1471
+ }
1472
+ if (issues.length > 0) {
1473
+ return payErr("Invalid request body", 400, void 0, issues);
1474
+ }
1475
+ const boltConnectionId = String(body.boltConnectionId);
1476
+ if (!findBoltConnection(boltConnectionId)) {
1477
+ return payErr("Bolt connection not found", 404, "NOT_FOUND");
1478
+ }
1479
+ const paymentMethodType = body.paymentMethod.paymentMethodType;
1480
+ const amount = body.amount;
1481
+ const currency = String(body.currency ?? "THB");
1482
+ const expiryDurationInSec = body.expiryDurationInSec;
1483
+ const id = nextBoltIntentId();
1484
+ const now3 = /* @__PURE__ */ new Date();
1485
+ const intent = {
1486
+ id,
1487
+ beamIntentId: id,
1488
+ boltConnectionId,
1489
+ amount,
1490
+ currency,
1491
+ paymentMethodType,
1492
+ referenceId: body.referenceId != null ? String(body.referenceId) : null,
1493
+ internalNote: body.internalNote != null ? String(body.internalNote) : null,
1494
+ status: "PENDING",
1495
+ isVirtual: false,
1496
+ environment: "sandbox",
1497
+ expiresAt: new Date(now3.getTime() + expiryDurationInSec * 1e3),
1498
+ settledAt: null,
1499
+ chargeId: null,
1500
+ failureReason: null,
1501
+ createdAt: now3
1502
+ };
1503
+ state.boltIntents.set(id, intent);
1504
+ return json({
1505
+ id,
1506
+ status: "PENDING",
1507
+ amount,
1508
+ currency,
1509
+ createdAt: now3.toISOString()
1510
+ });
1511
+ }
1512
+ if (!intentId && request.method === "GET") {
1513
+ const deploymentId = url.searchParams.get("deploymentId");
1514
+ if (!deploymentId) {
1515
+ return payErr("Missing deploymentId parameter", 400);
1516
+ }
1517
+ const now3 = /* @__PURE__ */ new Date();
1518
+ const statusParam = url.searchParams.get("status");
1519
+ let requestedStatuses;
1520
+ if (statusParam) {
1521
+ const parts = statusParam.split(",").map((s) => s.trim()).filter(Boolean);
1522
+ const parsed = [];
1523
+ for (const part of parts) {
1524
+ if (!isBoltIntentStatus(part)) {
1525
+ return payErr(`Invalid status: ${part}`, 400);
1526
+ }
1527
+ parsed.push(part);
1528
+ }
1529
+ requestedStatuses = parsed;
1530
+ }
1531
+ const boltConnectionId = url.searchParams.get("boltConnectionId") ?? void 0;
1532
+ const limitParam = url.searchParams.get("limit");
1533
+ let limit = 50;
1534
+ if (limitParam !== null) {
1535
+ const trimmed = limitParam.trim();
1536
+ if (!/^\d+$/.test(trimmed)) {
1537
+ return payErr("limit must be an integer between 1 and 100", 400);
1538
+ }
1539
+ const parsed = Number.parseInt(trimmed, 10);
1540
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) {
1541
+ return payErr("limit must be an integer between 1 and 100", 400);
1542
+ }
1543
+ limit = parsed;
1544
+ }
1545
+ let intents = [...state.boltIntents.values()];
1546
+ if (boltConnectionId) {
1547
+ intents = intents.filter((i) => i.boltConnectionId === boltConnectionId);
1548
+ }
1549
+ if (requestedStatuses && requestedStatuses.length > 0) {
1550
+ const storedWanted = new Set(storedStatusesForFilter(requestedStatuses));
1551
+ intents = intents.filter((i) => storedWanted.has(i.status));
1552
+ }
1553
+ let records = intents.map((i) => toBoltIntentRecord(i, now3));
1554
+ if (requestedStatuses && requestedStatuses.length > 0) {
1555
+ const wanted = new Set(requestedStatuses);
1556
+ records = records.filter((r) => wanted.has(r.status));
1557
+ }
1558
+ records = records.slice(0, limit);
1559
+ return json({ intents: records });
1560
+ }
1561
+ if (intentId && request.method === "DELETE") {
1562
+ const intent = findBoltIntent(intentId);
1563
+ if (!intent) return payErr("Bolt intent not found", 404, "NOT_FOUND");
1564
+ const derived = deriveBoltIntentStatus(intent.status, intent.expiresAt);
1565
+ if (derived !== "PENDING") {
1566
+ return payErr(`Bolt intent is ${derived}, only PENDING intents can be canceled`, 409);
1567
+ }
1568
+ intent.status = "CANCELED";
1569
+ intent.settledAt = /* @__PURE__ */ new Date();
1570
+ return json({ success: true });
1571
+ }
1572
+ }
1573
+ const chargesMatch = pathname.match(/^\/api\/store\/beam\/([^/]+)\/charges$/);
1574
+ if (chargesMatch && request.method === "GET") {
1575
+ const sourceId = url.searchParams.get("sourceId");
1576
+ if (!sourceId) {
1577
+ return payErr("Missing sourceId parameter", 400);
1578
+ }
1579
+ const charges = state.boltCharges.filter((c) => c.sourceId === sourceId);
1580
+ return json({ charges });
1581
+ }
1278
1582
  const beamProductsMatch = pathname.match(
1279
1583
  /^\/api\/store\/beam\/([^/]+)\/payment-links(?:\/([^/]+))?$/
1280
1584
  );
@@ -1409,12 +1713,70 @@ function createPayments() {
1409
1713
  };
1410
1714
  return deliverEvent(envelope, handler, options);
1411
1715
  },
1716
+ approveBoltIntent(intentId, options) {
1717
+ const intent = requirePendingBoltIntent(intentId);
1718
+ const chargeId = options?.chargeId ?? nextBoltChargeId();
1719
+ const charge = {
1720
+ id: chargeId,
1721
+ status: "SUCCEEDED",
1722
+ amount: intent.amount,
1723
+ currency: intent.currency,
1724
+ sourceId: intent.beamIntentId,
1725
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1726
+ };
1727
+ intent.status = "PAID";
1728
+ intent.chargeId = chargeId;
1729
+ intent.settledAt = /* @__PURE__ */ new Date();
1730
+ state.boltCharges.push(charge);
1731
+ return charge;
1732
+ },
1733
+ // No webhook on decline: Beam has no charge-failure event for bolt payments.
1734
+ declineBoltIntent(intentId, options) {
1735
+ const intent = requirePendingBoltIntent(intentId);
1736
+ const chargeId = nextBoltChargeId();
1737
+ const failureReason = options?.failureReason ?? "declined";
1738
+ const charge = {
1739
+ id: chargeId,
1740
+ status: "FAILED",
1741
+ amount: intent.amount,
1742
+ currency: intent.currency,
1743
+ sourceId: intent.beamIntentId,
1744
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1745
+ };
1746
+ intent.status = "FAILED";
1747
+ intent.chargeId = chargeId;
1748
+ intent.failureReason = failureReason;
1749
+ intent.settledAt = /* @__PURE__ */ new Date();
1750
+ state.boltCharges.push(charge);
1751
+ return charge;
1752
+ },
1753
+ // No webhook on expire: Beam does not notify apps when a bolt intent expires.
1754
+ expireBoltIntent(intentId) {
1755
+ const intent = requirePendingBoltIntent(intentId);
1756
+ intent.expiresAt = new Date(Date.now() - 1e3);
1757
+ },
1758
+ listBoltIntents() {
1759
+ const now3 = /* @__PURE__ */ new Date();
1760
+ return [...state.boltIntents.values()].map((i) => toBoltIntentRecord(i, now3));
1761
+ },
1762
+ getBoltIntent(intentId) {
1763
+ const intent = findBoltIntent(intentId);
1764
+ if (!intent) return void 0;
1765
+ return toBoltIntentRecord(intent);
1766
+ },
1412
1767
  clear() {
1413
1768
  state.checkouts = [];
1414
1769
  state.checkoutCounter = 0;
1415
1770
  state.sessionStatuses.clear();
1416
1771
  state.paymentLinks.clear();
1417
1772
  state.products = [];
1773
+ state.boltConnections.clear();
1774
+ state.boltUsedPairingCodes.clear();
1775
+ state.boltConnectionCounter = 0;
1776
+ state.boltIntents.clear();
1777
+ state.boltIntentCounter = 0;
1778
+ state.boltCharges = [];
1779
+ state.boltChargeCounter = 0;
1418
1780
  },
1419
1781
  get count() {
1420
1782
  return state.checkouts.length;
@@ -51,6 +51,13 @@ var state = globalSingleton("state", () => ({
51
51
  sessionStatuses: /* @__PURE__ */ new Map(),
52
52
  paymentLinks: /* @__PURE__ */ new Map(),
53
53
  products: [],
54
+ boltConnections: /* @__PURE__ */ new Map(),
55
+ boltUsedPairingCodes: /* @__PURE__ */ new Set(),
56
+ boltConnectionCounter: 0,
57
+ boltIntents: /* @__PURE__ */ new Map(),
58
+ boltIntentCounter: 0,
59
+ boltCharges: [],
60
+ boltChargeCounter: 0,
54
61
  uploads: [],
55
62
  uploadCounter: 0,
56
63
  storageFiles: /* @__PURE__ */ new Map(),
@@ -24,6 +24,13 @@ var state = globalSingleton("state", () => ({
24
24
  sessionStatuses: /* @__PURE__ */ new Map(),
25
25
  paymentLinks: /* @__PURE__ */ new Map(),
26
26
  products: [],
27
+ boltConnections: /* @__PURE__ */ new Map(),
28
+ boltUsedPairingCodes: /* @__PURE__ */ new Set(),
29
+ boltConnectionCounter: 0,
30
+ boltIntents: /* @__PURE__ */ new Map(),
31
+ boltIntentCounter: 0,
32
+ boltCharges: [],
33
+ boltChargeCounter: 0,
27
34
  uploads: [],
28
35
  uploadCounter: 0,
29
36
  storageFiles: /* @__PURE__ */ new Map(),