@stardeck-customer-apps/testing 0.8.0 → 0.10.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/LICENSE +21 -0
- package/SKILL.md +123 -5
- package/dist/config.d.mts +6 -2
- package/dist/config.d.ts +6 -2
- package/dist/config.js +75 -1
- package/dist/config.mjs +73 -1
- package/dist/data-store-manifest-Zwgcv0DJ.d.mts +25 -0
- package/dist/data-store-manifest-Zwgcv0DJ.d.ts +25 -0
- package/dist/index.d.mts +82 -2
- package/dist/index.d.ts +82 -2
- package/dist/index.js +453 -6
- package/dist/index.mjs +450 -6
- package/dist/next/headers-shim.js +8 -0
- package/dist/next/headers-shim.mjs +8 -0
- package/dist/setup.js +294 -4
- package/dist/setup.mjs +294 -4
- package/package.json +2 -2
package/dist/setup.js
CHANGED
|
@@ -33,6 +33,7 @@ function globalSingleton(key, create) {
|
|
|
33
33
|
// src/state.ts
|
|
34
34
|
var state = globalSingleton("state", () => ({
|
|
35
35
|
db: null,
|
|
36
|
+
lastHarnessDataStoreManifest: null,
|
|
36
37
|
currentUser: null,
|
|
37
38
|
sessions: /* @__PURE__ */ new Map(),
|
|
38
39
|
refreshSessions: /* @__PURE__ */ new Map(),
|
|
@@ -45,6 +46,13 @@ var state = globalSingleton("state", () => ({
|
|
|
45
46
|
sessionStatuses: /* @__PURE__ */ new Map(),
|
|
46
47
|
paymentLinks: /* @__PURE__ */ new Map(),
|
|
47
48
|
products: [],
|
|
49
|
+
boltConnections: /* @__PURE__ */ new Map(),
|
|
50
|
+
boltUsedPairingCodes: /* @__PURE__ */ new Set(),
|
|
51
|
+
boltConnectionCounter: 0,
|
|
52
|
+
boltIntents: /* @__PURE__ */ new Map(),
|
|
53
|
+
boltIntentCounter: 0,
|
|
54
|
+
boltCharges: [],
|
|
55
|
+
boltChargeCounter: 0,
|
|
48
56
|
uploads: [],
|
|
49
57
|
uploadCounter: 0,
|
|
50
58
|
storageFiles: /* @__PURE__ */ new Map(),
|
|
@@ -74,6 +82,7 @@ function requireDb() {
|
|
|
74
82
|
var TEST_DOMAIN_SUFFIX = ".stardeck.test";
|
|
75
83
|
var CONTROL_PLANE_TEST_URL = "https://control-plane.stardeck.test";
|
|
76
84
|
var DATA_STORE_TEST_HOST = "db.stardeck.test";
|
|
85
|
+
var DATA_STORE_TEST_URL = `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`;
|
|
77
86
|
var STORAGE_TEST_URL = "https://storage.stardeck.test";
|
|
78
87
|
var STORAGE_TEST_HOST = "storage.stardeck.test";
|
|
79
88
|
var TEST_ENV_DEFAULTS = {
|
|
@@ -82,7 +91,6 @@ var TEST_ENV_DEFAULTS = {
|
|
|
82
91
|
ORGANIZATION_ID: "00000000-0000-4000-8000-00000000000a",
|
|
83
92
|
PROJECT_ID: "00000000-0000-4000-8000-00000000000b",
|
|
84
93
|
DEPLOYMENT_ID: "00000000-0000-4000-8000-00000000000c",
|
|
85
|
-
DATA_STORE_URL: `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`,
|
|
86
94
|
STORAGE_URL: STORAGE_TEST_URL
|
|
87
95
|
};
|
|
88
96
|
|
|
@@ -983,8 +991,31 @@ var requestScopeStorage = globalSingleton(
|
|
|
983
991
|
var import_node_crypto4 = __toESM(require("crypto"));
|
|
984
992
|
|
|
985
993
|
// src/simulator/payments.ts
|
|
986
|
-
|
|
987
|
-
|
|
994
|
+
var BEAM_BOLT_PAYMENT_METHODS = [
|
|
995
|
+
"CARD",
|
|
996
|
+
"CARD_INSTALLMENTS",
|
|
997
|
+
"QR_PROMPT_PAY",
|
|
998
|
+
"ALIPAY",
|
|
999
|
+
"ALIPAY_PLUS",
|
|
1000
|
+
"LINE_PAY",
|
|
1001
|
+
"SHOPEE_PAY",
|
|
1002
|
+
"TRUE_MONEY",
|
|
1003
|
+
"WECHAT_PAY",
|
|
1004
|
+
"SPAY_LATER"
|
|
1005
|
+
];
|
|
1006
|
+
var BEAM_BOLT_INTENT_STATUSES = ["PENDING", "PAID", "FAILED", "CANCELED", "EXPIRED"];
|
|
1007
|
+
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;
|
|
1008
|
+
function payErr(error, status = 400, code, details) {
|
|
1009
|
+
const body = { error };
|
|
1010
|
+
if (code) body.code = code;
|
|
1011
|
+
if (details !== void 0) body.details = details;
|
|
1012
|
+
return json(body, status);
|
|
1013
|
+
}
|
|
1014
|
+
function isBoltPaymentMethod(value) {
|
|
1015
|
+
return typeof value === "string" && BEAM_BOLT_PAYMENT_METHODS.includes(value);
|
|
1016
|
+
}
|
|
1017
|
+
function isBoltIntentStatus(value) {
|
|
1018
|
+
return BEAM_BOLT_INTENT_STATUSES.includes(value);
|
|
988
1019
|
}
|
|
989
1020
|
function nextCheckoutId() {
|
|
990
1021
|
state.checkoutCounter += 1;
|
|
@@ -994,6 +1025,14 @@ function nextPaymentLinkId() {
|
|
|
994
1025
|
state.checkoutCounter += 1;
|
|
995
1026
|
return `plink_test_${state.checkoutCounter}`;
|
|
996
1027
|
}
|
|
1028
|
+
function nextBoltConnectionId() {
|
|
1029
|
+
state.boltConnectionCounter += 1;
|
|
1030
|
+
return `boltc_${state.boltConnectionCounter}`;
|
|
1031
|
+
}
|
|
1032
|
+
function nextBoltIntentId() {
|
|
1033
|
+
state.boltIntentCounter += 1;
|
|
1034
|
+
return `bolti_${state.boltIntentCounter}`;
|
|
1035
|
+
}
|
|
997
1036
|
function seedStripeSession(id, body) {
|
|
998
1037
|
const lineItems = body.lineItems ?? [];
|
|
999
1038
|
let amountTotal = null;
|
|
@@ -1037,11 +1076,262 @@ function seedBeamLink(id, body, merchantId) {
|
|
|
1037
1076
|
collectDeliveryAddress: body.collectDeliveryAddress === true
|
|
1038
1077
|
});
|
|
1039
1078
|
}
|
|
1079
|
+
function deriveBoltIntentStatus(storedStatus, expiresAt, now3 = /* @__PURE__ */ new Date()) {
|
|
1080
|
+
if (storedStatus !== "PENDING") return storedStatus;
|
|
1081
|
+
return expiresAt.getTime() <= now3.getTime() ? "EXPIRED" : "PENDING";
|
|
1082
|
+
}
|
|
1083
|
+
function toBoltIntentRecord(intent, now3 = /* @__PURE__ */ new Date()) {
|
|
1084
|
+
return {
|
|
1085
|
+
id: intent.id,
|
|
1086
|
+
beamIntentId: intent.beamIntentId,
|
|
1087
|
+
boltConnectionId: intent.boltConnectionId,
|
|
1088
|
+
amount: intent.amount,
|
|
1089
|
+
currency: intent.currency,
|
|
1090
|
+
paymentMethodType: intent.paymentMethodType,
|
|
1091
|
+
referenceId: intent.referenceId,
|
|
1092
|
+
internalNote: intent.internalNote,
|
|
1093
|
+
status: deriveBoltIntentStatus(intent.status, intent.expiresAt, now3),
|
|
1094
|
+
isVirtual: intent.isVirtual,
|
|
1095
|
+
environment: intent.environment,
|
|
1096
|
+
expiresAt: intent.expiresAt.toISOString(),
|
|
1097
|
+
settledAt: intent.settledAt ? intent.settledAt.toISOString() : null,
|
|
1098
|
+
chargeId: intent.chargeId,
|
|
1099
|
+
failureReason: intent.failureReason,
|
|
1100
|
+
createdAt: intent.createdAt.toISOString()
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
function findBoltConnection(connectionId) {
|
|
1104
|
+
return state.boltConnections.get(connectionId) ?? [...state.boltConnections.values()].find((c) => c.beamConnectionId === connectionId);
|
|
1105
|
+
}
|
|
1106
|
+
function findBoltIntent(intentId) {
|
|
1107
|
+
return state.boltIntents.get(intentId) ?? [...state.boltIntents.values()].find((i) => i.beamIntentId === intentId);
|
|
1108
|
+
}
|
|
1109
|
+
function storedStatusesForFilter(statuses) {
|
|
1110
|
+
const stored = /* @__PURE__ */ new Set();
|
|
1111
|
+
for (const status of statuses) {
|
|
1112
|
+
if (status === "EXPIRED" || status === "PENDING") {
|
|
1113
|
+
stored.add("PENDING");
|
|
1114
|
+
} else {
|
|
1115
|
+
stored.add(status);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
return [...stored];
|
|
1119
|
+
}
|
|
1040
1120
|
async function handlePaymentsRequest(request, url) {
|
|
1041
1121
|
const pathname = url.pathname;
|
|
1042
|
-
if (/\/
|
|
1122
|
+
if (/\/billing-portal$/.test(pathname)) {
|
|
1043
1123
|
return payErr("Not found", 404, "NOT_FOUND");
|
|
1044
1124
|
}
|
|
1125
|
+
const boltConnectionsMatch = pathname.match(
|
|
1126
|
+
/^\/api\/store\/beam\/([^/]+)\/bolt-connections(?:\/([^/]+))?$/
|
|
1127
|
+
);
|
|
1128
|
+
if (boltConnectionsMatch) {
|
|
1129
|
+
const connectionId = boltConnectionsMatch[2];
|
|
1130
|
+
if (!connectionId && request.method === "POST") {
|
|
1131
|
+
const body = await readJsonBody(request);
|
|
1132
|
+
const pairingCode = body.pairingCode ? String(body.pairingCode) : "";
|
|
1133
|
+
if (!pairingCode) {
|
|
1134
|
+
return payErr("pairingCode is required", 400);
|
|
1135
|
+
}
|
|
1136
|
+
if (state.boltUsedPairingCodes.has(pairingCode)) {
|
|
1137
|
+
return payErr("Pairing code has already been used", 400, "PAIRING_CODE_USED");
|
|
1138
|
+
}
|
|
1139
|
+
const id = nextBoltConnectionId();
|
|
1140
|
+
const now3 = (/* @__PURE__ */ new Date()).toISOString();
|
|
1141
|
+
const environments = Array.isArray(body.environments) ? body.environments.map(String) : ["sandbox"];
|
|
1142
|
+
const isSandbox = environments.some((e) => e === "sandbox" || e === "preview");
|
|
1143
|
+
const connection = {
|
|
1144
|
+
id,
|
|
1145
|
+
projectId: TEST_ENV_DEFAULTS.PROJECT_ID,
|
|
1146
|
+
beamConnectionId: id,
|
|
1147
|
+
displayName: body.displayName != null ? String(body.displayName) : null,
|
|
1148
|
+
pairingCode,
|
|
1149
|
+
status: "ACTIVE",
|
|
1150
|
+
isSandbox,
|
|
1151
|
+
environments,
|
|
1152
|
+
createdAt: now3,
|
|
1153
|
+
updatedAt: now3
|
|
1154
|
+
};
|
|
1155
|
+
state.boltUsedPairingCodes.add(pairingCode);
|
|
1156
|
+
state.boltConnections.set(id, connection);
|
|
1157
|
+
return json({ connection });
|
|
1158
|
+
}
|
|
1159
|
+
if (!connectionId && request.method === "GET") {
|
|
1160
|
+
return json({ connections: [...state.boltConnections.values()] });
|
|
1161
|
+
}
|
|
1162
|
+
if (connectionId && request.method === "GET") {
|
|
1163
|
+
const connection = findBoltConnection(connectionId);
|
|
1164
|
+
if (!connection) return payErr("Bolt connection not found", 404, "NOT_FOUND");
|
|
1165
|
+
return json({ connection });
|
|
1166
|
+
}
|
|
1167
|
+
if (connectionId && request.method === "DELETE") {
|
|
1168
|
+
const connection = findBoltConnection(connectionId);
|
|
1169
|
+
if (!connection) return payErr("Bolt connection not found", 404, "NOT_FOUND");
|
|
1170
|
+
state.boltConnections.delete(connection.id);
|
|
1171
|
+
return json({ success: true });
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
const boltIntentsMatch = pathname.match(
|
|
1175
|
+
/^\/api\/store\/beam\/([^/]+)\/bolt-intents(?:\/([^/]+))?$/
|
|
1176
|
+
);
|
|
1177
|
+
if (boltIntentsMatch) {
|
|
1178
|
+
const intentId = boltIntentsMatch[2];
|
|
1179
|
+
if (!intentId && request.method === "POST") {
|
|
1180
|
+
const body = await readJsonBody(request);
|
|
1181
|
+
const issues = [];
|
|
1182
|
+
if (typeof body.amount !== "number" || !Number.isInteger(body.amount) || body.amount < 1) {
|
|
1183
|
+
issues.push({
|
|
1184
|
+
path: ["amount"],
|
|
1185
|
+
message: "Number must be greater than 0",
|
|
1186
|
+
code: "too_small"
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
const boltConnectionIdRaw = body.boltConnectionId;
|
|
1190
|
+
if (typeof boltConnectionIdRaw !== "string" || boltConnectionIdRaw.length < 1) {
|
|
1191
|
+
issues.push({
|
|
1192
|
+
path: ["boltConnectionId"],
|
|
1193
|
+
message: "String must contain at least 1 character(s)",
|
|
1194
|
+
code: "too_small"
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
const paymentMethod = body.paymentMethod;
|
|
1198
|
+
if (!paymentMethod || typeof paymentMethod !== "object") {
|
|
1199
|
+
issues.push({
|
|
1200
|
+
path: ["paymentMethod"],
|
|
1201
|
+
message: "Required",
|
|
1202
|
+
code: "invalid_type"
|
|
1203
|
+
});
|
|
1204
|
+
} else if (!isBoltPaymentMethod(paymentMethod.paymentMethodType)) {
|
|
1205
|
+
issues.push({
|
|
1206
|
+
path: ["paymentMethod", "paymentMethodType"],
|
|
1207
|
+
message: "Invalid enum value",
|
|
1208
|
+
code: "invalid_enum_value"
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
if (typeof body.expiryDurationInSec !== "number" || !Number.isInteger(body.expiryDurationInSec) || body.expiryDurationInSec < 90 || body.expiryDurationInSec > 600) {
|
|
1212
|
+
issues.push({
|
|
1213
|
+
path: ["expiryDurationInSec"],
|
|
1214
|
+
message: "Number must be between 90 and 600",
|
|
1215
|
+
code: "too_small"
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
if (typeof body.deploymentId !== "string" || !UUID_RE.test(body.deploymentId)) {
|
|
1219
|
+
issues.push({
|
|
1220
|
+
path: ["deploymentId"],
|
|
1221
|
+
message: "Invalid uuid",
|
|
1222
|
+
code: "invalid_string"
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
if (issues.length > 0) {
|
|
1226
|
+
return payErr("Invalid request body", 400, void 0, issues);
|
|
1227
|
+
}
|
|
1228
|
+
const boltConnectionId = String(body.boltConnectionId);
|
|
1229
|
+
if (!findBoltConnection(boltConnectionId)) {
|
|
1230
|
+
return payErr("Bolt connection not found", 404, "NOT_FOUND");
|
|
1231
|
+
}
|
|
1232
|
+
const paymentMethodType = body.paymentMethod.paymentMethodType;
|
|
1233
|
+
const amount = body.amount;
|
|
1234
|
+
const currency = String(body.currency ?? "THB");
|
|
1235
|
+
const expiryDurationInSec = body.expiryDurationInSec;
|
|
1236
|
+
const id = nextBoltIntentId();
|
|
1237
|
+
const now3 = /* @__PURE__ */ new Date();
|
|
1238
|
+
const intent = {
|
|
1239
|
+
id,
|
|
1240
|
+
beamIntentId: id,
|
|
1241
|
+
boltConnectionId,
|
|
1242
|
+
amount,
|
|
1243
|
+
currency,
|
|
1244
|
+
paymentMethodType,
|
|
1245
|
+
referenceId: body.referenceId != null ? String(body.referenceId) : null,
|
|
1246
|
+
internalNote: body.internalNote != null ? String(body.internalNote) : null,
|
|
1247
|
+
status: "PENDING",
|
|
1248
|
+
isVirtual: false,
|
|
1249
|
+
environment: "sandbox",
|
|
1250
|
+
expiresAt: new Date(now3.getTime() + expiryDurationInSec * 1e3),
|
|
1251
|
+
settledAt: null,
|
|
1252
|
+
chargeId: null,
|
|
1253
|
+
failureReason: null,
|
|
1254
|
+
createdAt: now3
|
|
1255
|
+
};
|
|
1256
|
+
state.boltIntents.set(id, intent);
|
|
1257
|
+
return json({
|
|
1258
|
+
id,
|
|
1259
|
+
status: "PENDING",
|
|
1260
|
+
amount,
|
|
1261
|
+
currency,
|
|
1262
|
+
createdAt: now3.toISOString()
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
if (!intentId && request.method === "GET") {
|
|
1266
|
+
const deploymentId = url.searchParams.get("deploymentId");
|
|
1267
|
+
if (!deploymentId) {
|
|
1268
|
+
return payErr("Missing deploymentId parameter", 400);
|
|
1269
|
+
}
|
|
1270
|
+
const now3 = /* @__PURE__ */ new Date();
|
|
1271
|
+
const statusParam = url.searchParams.get("status");
|
|
1272
|
+
let requestedStatuses;
|
|
1273
|
+
if (statusParam) {
|
|
1274
|
+
const parts = statusParam.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1275
|
+
const parsed = [];
|
|
1276
|
+
for (const part of parts) {
|
|
1277
|
+
if (!isBoltIntentStatus(part)) {
|
|
1278
|
+
return payErr(`Invalid status: ${part}`, 400);
|
|
1279
|
+
}
|
|
1280
|
+
parsed.push(part);
|
|
1281
|
+
}
|
|
1282
|
+
requestedStatuses = parsed;
|
|
1283
|
+
}
|
|
1284
|
+
const boltConnectionId = url.searchParams.get("boltConnectionId") ?? void 0;
|
|
1285
|
+
const limitParam = url.searchParams.get("limit");
|
|
1286
|
+
let limit = 50;
|
|
1287
|
+
if (limitParam !== null) {
|
|
1288
|
+
const trimmed = limitParam.trim();
|
|
1289
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
1290
|
+
return payErr("limit must be an integer between 1 and 100", 400);
|
|
1291
|
+
}
|
|
1292
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
1293
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) {
|
|
1294
|
+
return payErr("limit must be an integer between 1 and 100", 400);
|
|
1295
|
+
}
|
|
1296
|
+
limit = parsed;
|
|
1297
|
+
}
|
|
1298
|
+
let intents = [...state.boltIntents.values()];
|
|
1299
|
+
if (boltConnectionId) {
|
|
1300
|
+
intents = intents.filter((i) => i.boltConnectionId === boltConnectionId);
|
|
1301
|
+
}
|
|
1302
|
+
if (requestedStatuses && requestedStatuses.length > 0) {
|
|
1303
|
+
const storedWanted = new Set(storedStatusesForFilter(requestedStatuses));
|
|
1304
|
+
intents = intents.filter((i) => storedWanted.has(i.status));
|
|
1305
|
+
}
|
|
1306
|
+
let records = intents.map((i) => toBoltIntentRecord(i, now3));
|
|
1307
|
+
if (requestedStatuses && requestedStatuses.length > 0) {
|
|
1308
|
+
const wanted = new Set(requestedStatuses);
|
|
1309
|
+
records = records.filter((r) => wanted.has(r.status));
|
|
1310
|
+
}
|
|
1311
|
+
records = records.slice(0, limit);
|
|
1312
|
+
return json({ intents: records });
|
|
1313
|
+
}
|
|
1314
|
+
if (intentId && request.method === "DELETE") {
|
|
1315
|
+
const intent = findBoltIntent(intentId);
|
|
1316
|
+
if (!intent) return payErr("Bolt intent not found", 404, "NOT_FOUND");
|
|
1317
|
+
const derived = deriveBoltIntentStatus(intent.status, intent.expiresAt);
|
|
1318
|
+
if (derived !== "PENDING") {
|
|
1319
|
+
return payErr(`Bolt intent is ${derived}, only PENDING intents can be canceled`, 409);
|
|
1320
|
+
}
|
|
1321
|
+
intent.status = "CANCELED";
|
|
1322
|
+
intent.settledAt = /* @__PURE__ */ new Date();
|
|
1323
|
+
return json({ success: true });
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
const chargesMatch = pathname.match(/^\/api\/store\/beam\/([^/]+)\/charges$/);
|
|
1327
|
+
if (chargesMatch && request.method === "GET") {
|
|
1328
|
+
const sourceId = url.searchParams.get("sourceId");
|
|
1329
|
+
if (!sourceId) {
|
|
1330
|
+
return payErr("Missing sourceId parameter", 400);
|
|
1331
|
+
}
|
|
1332
|
+
const charges = state.boltCharges.filter((c) => c.sourceId === sourceId);
|
|
1333
|
+
return json({ charges });
|
|
1334
|
+
}
|
|
1045
1335
|
const beamProductsMatch = pathname.match(
|
|
1046
1336
|
/^\/api\/store\/beam\/([^/]+)\/payment-links(?:\/([^/]+))?$/
|
|
1047
1337
|
);
|
package/dist/setup.mjs
CHANGED
|
@@ -9,6 +9,7 @@ function globalSingleton(key, create) {
|
|
|
9
9
|
// src/state.ts
|
|
10
10
|
var state = globalSingleton("state", () => ({
|
|
11
11
|
db: null,
|
|
12
|
+
lastHarnessDataStoreManifest: null,
|
|
12
13
|
currentUser: null,
|
|
13
14
|
sessions: /* @__PURE__ */ new Map(),
|
|
14
15
|
refreshSessions: /* @__PURE__ */ new Map(),
|
|
@@ -21,6 +22,13 @@ var state = globalSingleton("state", () => ({
|
|
|
21
22
|
sessionStatuses: /* @__PURE__ */ new Map(),
|
|
22
23
|
paymentLinks: /* @__PURE__ */ new Map(),
|
|
23
24
|
products: [],
|
|
25
|
+
boltConnections: /* @__PURE__ */ new Map(),
|
|
26
|
+
boltUsedPairingCodes: /* @__PURE__ */ new Set(),
|
|
27
|
+
boltConnectionCounter: 0,
|
|
28
|
+
boltIntents: /* @__PURE__ */ new Map(),
|
|
29
|
+
boltIntentCounter: 0,
|
|
30
|
+
boltCharges: [],
|
|
31
|
+
boltChargeCounter: 0,
|
|
24
32
|
uploads: [],
|
|
25
33
|
uploadCounter: 0,
|
|
26
34
|
storageFiles: /* @__PURE__ */ new Map(),
|
|
@@ -50,6 +58,7 @@ function requireDb() {
|
|
|
50
58
|
var TEST_DOMAIN_SUFFIX = ".stardeck.test";
|
|
51
59
|
var CONTROL_PLANE_TEST_URL = "https://control-plane.stardeck.test";
|
|
52
60
|
var DATA_STORE_TEST_HOST = "db.stardeck.test";
|
|
61
|
+
var DATA_STORE_TEST_URL = `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`;
|
|
53
62
|
var STORAGE_TEST_URL = "https://storage.stardeck.test";
|
|
54
63
|
var STORAGE_TEST_HOST = "storage.stardeck.test";
|
|
55
64
|
var TEST_ENV_DEFAULTS = {
|
|
@@ -58,7 +67,6 @@ var TEST_ENV_DEFAULTS = {
|
|
|
58
67
|
ORGANIZATION_ID: "00000000-0000-4000-8000-00000000000a",
|
|
59
68
|
PROJECT_ID: "00000000-0000-4000-8000-00000000000b",
|
|
60
69
|
DEPLOYMENT_ID: "00000000-0000-4000-8000-00000000000c",
|
|
61
|
-
DATA_STORE_URL: `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`,
|
|
62
70
|
STORAGE_URL: STORAGE_TEST_URL
|
|
63
71
|
};
|
|
64
72
|
|
|
@@ -959,8 +967,31 @@ var requestScopeStorage = globalSingleton(
|
|
|
959
967
|
import crypto4 from "crypto";
|
|
960
968
|
|
|
961
969
|
// src/simulator/payments.ts
|
|
962
|
-
|
|
963
|
-
|
|
970
|
+
var BEAM_BOLT_PAYMENT_METHODS = [
|
|
971
|
+
"CARD",
|
|
972
|
+
"CARD_INSTALLMENTS",
|
|
973
|
+
"QR_PROMPT_PAY",
|
|
974
|
+
"ALIPAY",
|
|
975
|
+
"ALIPAY_PLUS",
|
|
976
|
+
"LINE_PAY",
|
|
977
|
+
"SHOPEE_PAY",
|
|
978
|
+
"TRUE_MONEY",
|
|
979
|
+
"WECHAT_PAY",
|
|
980
|
+
"SPAY_LATER"
|
|
981
|
+
];
|
|
982
|
+
var BEAM_BOLT_INTENT_STATUSES = ["PENDING", "PAID", "FAILED", "CANCELED", "EXPIRED"];
|
|
983
|
+
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;
|
|
984
|
+
function payErr(error, status = 400, code, details) {
|
|
985
|
+
const body = { error };
|
|
986
|
+
if (code) body.code = code;
|
|
987
|
+
if (details !== void 0) body.details = details;
|
|
988
|
+
return json(body, status);
|
|
989
|
+
}
|
|
990
|
+
function isBoltPaymentMethod(value) {
|
|
991
|
+
return typeof value === "string" && BEAM_BOLT_PAYMENT_METHODS.includes(value);
|
|
992
|
+
}
|
|
993
|
+
function isBoltIntentStatus(value) {
|
|
994
|
+
return BEAM_BOLT_INTENT_STATUSES.includes(value);
|
|
964
995
|
}
|
|
965
996
|
function nextCheckoutId() {
|
|
966
997
|
state.checkoutCounter += 1;
|
|
@@ -970,6 +1001,14 @@ function nextPaymentLinkId() {
|
|
|
970
1001
|
state.checkoutCounter += 1;
|
|
971
1002
|
return `plink_test_${state.checkoutCounter}`;
|
|
972
1003
|
}
|
|
1004
|
+
function nextBoltConnectionId() {
|
|
1005
|
+
state.boltConnectionCounter += 1;
|
|
1006
|
+
return `boltc_${state.boltConnectionCounter}`;
|
|
1007
|
+
}
|
|
1008
|
+
function nextBoltIntentId() {
|
|
1009
|
+
state.boltIntentCounter += 1;
|
|
1010
|
+
return `bolti_${state.boltIntentCounter}`;
|
|
1011
|
+
}
|
|
973
1012
|
function seedStripeSession(id, body) {
|
|
974
1013
|
const lineItems = body.lineItems ?? [];
|
|
975
1014
|
let amountTotal = null;
|
|
@@ -1013,11 +1052,262 @@ function seedBeamLink(id, body, merchantId) {
|
|
|
1013
1052
|
collectDeliveryAddress: body.collectDeliveryAddress === true
|
|
1014
1053
|
});
|
|
1015
1054
|
}
|
|
1055
|
+
function deriveBoltIntentStatus(storedStatus, expiresAt, now3 = /* @__PURE__ */ new Date()) {
|
|
1056
|
+
if (storedStatus !== "PENDING") return storedStatus;
|
|
1057
|
+
return expiresAt.getTime() <= now3.getTime() ? "EXPIRED" : "PENDING";
|
|
1058
|
+
}
|
|
1059
|
+
function toBoltIntentRecord(intent, now3 = /* @__PURE__ */ new Date()) {
|
|
1060
|
+
return {
|
|
1061
|
+
id: intent.id,
|
|
1062
|
+
beamIntentId: intent.beamIntentId,
|
|
1063
|
+
boltConnectionId: intent.boltConnectionId,
|
|
1064
|
+
amount: intent.amount,
|
|
1065
|
+
currency: intent.currency,
|
|
1066
|
+
paymentMethodType: intent.paymentMethodType,
|
|
1067
|
+
referenceId: intent.referenceId,
|
|
1068
|
+
internalNote: intent.internalNote,
|
|
1069
|
+
status: deriveBoltIntentStatus(intent.status, intent.expiresAt, now3),
|
|
1070
|
+
isVirtual: intent.isVirtual,
|
|
1071
|
+
environment: intent.environment,
|
|
1072
|
+
expiresAt: intent.expiresAt.toISOString(),
|
|
1073
|
+
settledAt: intent.settledAt ? intent.settledAt.toISOString() : null,
|
|
1074
|
+
chargeId: intent.chargeId,
|
|
1075
|
+
failureReason: intent.failureReason,
|
|
1076
|
+
createdAt: intent.createdAt.toISOString()
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
function findBoltConnection(connectionId) {
|
|
1080
|
+
return state.boltConnections.get(connectionId) ?? [...state.boltConnections.values()].find((c) => c.beamConnectionId === connectionId);
|
|
1081
|
+
}
|
|
1082
|
+
function findBoltIntent(intentId) {
|
|
1083
|
+
return state.boltIntents.get(intentId) ?? [...state.boltIntents.values()].find((i) => i.beamIntentId === intentId);
|
|
1084
|
+
}
|
|
1085
|
+
function storedStatusesForFilter(statuses) {
|
|
1086
|
+
const stored = /* @__PURE__ */ new Set();
|
|
1087
|
+
for (const status of statuses) {
|
|
1088
|
+
if (status === "EXPIRED" || status === "PENDING") {
|
|
1089
|
+
stored.add("PENDING");
|
|
1090
|
+
} else {
|
|
1091
|
+
stored.add(status);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
return [...stored];
|
|
1095
|
+
}
|
|
1016
1096
|
async function handlePaymentsRequest(request, url) {
|
|
1017
1097
|
const pathname = url.pathname;
|
|
1018
|
-
if (/\/
|
|
1098
|
+
if (/\/billing-portal$/.test(pathname)) {
|
|
1019
1099
|
return payErr("Not found", 404, "NOT_FOUND");
|
|
1020
1100
|
}
|
|
1101
|
+
const boltConnectionsMatch = pathname.match(
|
|
1102
|
+
/^\/api\/store\/beam\/([^/]+)\/bolt-connections(?:\/([^/]+))?$/
|
|
1103
|
+
);
|
|
1104
|
+
if (boltConnectionsMatch) {
|
|
1105
|
+
const connectionId = boltConnectionsMatch[2];
|
|
1106
|
+
if (!connectionId && request.method === "POST") {
|
|
1107
|
+
const body = await readJsonBody(request);
|
|
1108
|
+
const pairingCode = body.pairingCode ? String(body.pairingCode) : "";
|
|
1109
|
+
if (!pairingCode) {
|
|
1110
|
+
return payErr("pairingCode is required", 400);
|
|
1111
|
+
}
|
|
1112
|
+
if (state.boltUsedPairingCodes.has(pairingCode)) {
|
|
1113
|
+
return payErr("Pairing code has already been used", 400, "PAIRING_CODE_USED");
|
|
1114
|
+
}
|
|
1115
|
+
const id = nextBoltConnectionId();
|
|
1116
|
+
const now3 = (/* @__PURE__ */ new Date()).toISOString();
|
|
1117
|
+
const environments = Array.isArray(body.environments) ? body.environments.map(String) : ["sandbox"];
|
|
1118
|
+
const isSandbox = environments.some((e) => e === "sandbox" || e === "preview");
|
|
1119
|
+
const connection = {
|
|
1120
|
+
id,
|
|
1121
|
+
projectId: TEST_ENV_DEFAULTS.PROJECT_ID,
|
|
1122
|
+
beamConnectionId: id,
|
|
1123
|
+
displayName: body.displayName != null ? String(body.displayName) : null,
|
|
1124
|
+
pairingCode,
|
|
1125
|
+
status: "ACTIVE",
|
|
1126
|
+
isSandbox,
|
|
1127
|
+
environments,
|
|
1128
|
+
createdAt: now3,
|
|
1129
|
+
updatedAt: now3
|
|
1130
|
+
};
|
|
1131
|
+
state.boltUsedPairingCodes.add(pairingCode);
|
|
1132
|
+
state.boltConnections.set(id, connection);
|
|
1133
|
+
return json({ connection });
|
|
1134
|
+
}
|
|
1135
|
+
if (!connectionId && request.method === "GET") {
|
|
1136
|
+
return json({ connections: [...state.boltConnections.values()] });
|
|
1137
|
+
}
|
|
1138
|
+
if (connectionId && request.method === "GET") {
|
|
1139
|
+
const connection = findBoltConnection(connectionId);
|
|
1140
|
+
if (!connection) return payErr("Bolt connection not found", 404, "NOT_FOUND");
|
|
1141
|
+
return json({ connection });
|
|
1142
|
+
}
|
|
1143
|
+
if (connectionId && request.method === "DELETE") {
|
|
1144
|
+
const connection = findBoltConnection(connectionId);
|
|
1145
|
+
if (!connection) return payErr("Bolt connection not found", 404, "NOT_FOUND");
|
|
1146
|
+
state.boltConnections.delete(connection.id);
|
|
1147
|
+
return json({ success: true });
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
const boltIntentsMatch = pathname.match(
|
|
1151
|
+
/^\/api\/store\/beam\/([^/]+)\/bolt-intents(?:\/([^/]+))?$/
|
|
1152
|
+
);
|
|
1153
|
+
if (boltIntentsMatch) {
|
|
1154
|
+
const intentId = boltIntentsMatch[2];
|
|
1155
|
+
if (!intentId && request.method === "POST") {
|
|
1156
|
+
const body = await readJsonBody(request);
|
|
1157
|
+
const issues = [];
|
|
1158
|
+
if (typeof body.amount !== "number" || !Number.isInteger(body.amount) || body.amount < 1) {
|
|
1159
|
+
issues.push({
|
|
1160
|
+
path: ["amount"],
|
|
1161
|
+
message: "Number must be greater than 0",
|
|
1162
|
+
code: "too_small"
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
const boltConnectionIdRaw = body.boltConnectionId;
|
|
1166
|
+
if (typeof boltConnectionIdRaw !== "string" || boltConnectionIdRaw.length < 1) {
|
|
1167
|
+
issues.push({
|
|
1168
|
+
path: ["boltConnectionId"],
|
|
1169
|
+
message: "String must contain at least 1 character(s)",
|
|
1170
|
+
code: "too_small"
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
const paymentMethod = body.paymentMethod;
|
|
1174
|
+
if (!paymentMethod || typeof paymentMethod !== "object") {
|
|
1175
|
+
issues.push({
|
|
1176
|
+
path: ["paymentMethod"],
|
|
1177
|
+
message: "Required",
|
|
1178
|
+
code: "invalid_type"
|
|
1179
|
+
});
|
|
1180
|
+
} else if (!isBoltPaymentMethod(paymentMethod.paymentMethodType)) {
|
|
1181
|
+
issues.push({
|
|
1182
|
+
path: ["paymentMethod", "paymentMethodType"],
|
|
1183
|
+
message: "Invalid enum value",
|
|
1184
|
+
code: "invalid_enum_value"
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
if (typeof body.expiryDurationInSec !== "number" || !Number.isInteger(body.expiryDurationInSec) || body.expiryDurationInSec < 90 || body.expiryDurationInSec > 600) {
|
|
1188
|
+
issues.push({
|
|
1189
|
+
path: ["expiryDurationInSec"],
|
|
1190
|
+
message: "Number must be between 90 and 600",
|
|
1191
|
+
code: "too_small"
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
if (typeof body.deploymentId !== "string" || !UUID_RE.test(body.deploymentId)) {
|
|
1195
|
+
issues.push({
|
|
1196
|
+
path: ["deploymentId"],
|
|
1197
|
+
message: "Invalid uuid",
|
|
1198
|
+
code: "invalid_string"
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
if (issues.length > 0) {
|
|
1202
|
+
return payErr("Invalid request body", 400, void 0, issues);
|
|
1203
|
+
}
|
|
1204
|
+
const boltConnectionId = String(body.boltConnectionId);
|
|
1205
|
+
if (!findBoltConnection(boltConnectionId)) {
|
|
1206
|
+
return payErr("Bolt connection not found", 404, "NOT_FOUND");
|
|
1207
|
+
}
|
|
1208
|
+
const paymentMethodType = body.paymentMethod.paymentMethodType;
|
|
1209
|
+
const amount = body.amount;
|
|
1210
|
+
const currency = String(body.currency ?? "THB");
|
|
1211
|
+
const expiryDurationInSec = body.expiryDurationInSec;
|
|
1212
|
+
const id = nextBoltIntentId();
|
|
1213
|
+
const now3 = /* @__PURE__ */ new Date();
|
|
1214
|
+
const intent = {
|
|
1215
|
+
id,
|
|
1216
|
+
beamIntentId: id,
|
|
1217
|
+
boltConnectionId,
|
|
1218
|
+
amount,
|
|
1219
|
+
currency,
|
|
1220
|
+
paymentMethodType,
|
|
1221
|
+
referenceId: body.referenceId != null ? String(body.referenceId) : null,
|
|
1222
|
+
internalNote: body.internalNote != null ? String(body.internalNote) : null,
|
|
1223
|
+
status: "PENDING",
|
|
1224
|
+
isVirtual: false,
|
|
1225
|
+
environment: "sandbox",
|
|
1226
|
+
expiresAt: new Date(now3.getTime() + expiryDurationInSec * 1e3),
|
|
1227
|
+
settledAt: null,
|
|
1228
|
+
chargeId: null,
|
|
1229
|
+
failureReason: null,
|
|
1230
|
+
createdAt: now3
|
|
1231
|
+
};
|
|
1232
|
+
state.boltIntents.set(id, intent);
|
|
1233
|
+
return json({
|
|
1234
|
+
id,
|
|
1235
|
+
status: "PENDING",
|
|
1236
|
+
amount,
|
|
1237
|
+
currency,
|
|
1238
|
+
createdAt: now3.toISOString()
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
if (!intentId && request.method === "GET") {
|
|
1242
|
+
const deploymentId = url.searchParams.get("deploymentId");
|
|
1243
|
+
if (!deploymentId) {
|
|
1244
|
+
return payErr("Missing deploymentId parameter", 400);
|
|
1245
|
+
}
|
|
1246
|
+
const now3 = /* @__PURE__ */ new Date();
|
|
1247
|
+
const statusParam = url.searchParams.get("status");
|
|
1248
|
+
let requestedStatuses;
|
|
1249
|
+
if (statusParam) {
|
|
1250
|
+
const parts = statusParam.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1251
|
+
const parsed = [];
|
|
1252
|
+
for (const part of parts) {
|
|
1253
|
+
if (!isBoltIntentStatus(part)) {
|
|
1254
|
+
return payErr(`Invalid status: ${part}`, 400);
|
|
1255
|
+
}
|
|
1256
|
+
parsed.push(part);
|
|
1257
|
+
}
|
|
1258
|
+
requestedStatuses = parsed;
|
|
1259
|
+
}
|
|
1260
|
+
const boltConnectionId = url.searchParams.get("boltConnectionId") ?? void 0;
|
|
1261
|
+
const limitParam = url.searchParams.get("limit");
|
|
1262
|
+
let limit = 50;
|
|
1263
|
+
if (limitParam !== null) {
|
|
1264
|
+
const trimmed = limitParam.trim();
|
|
1265
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
1266
|
+
return payErr("limit must be an integer between 1 and 100", 400);
|
|
1267
|
+
}
|
|
1268
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
1269
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) {
|
|
1270
|
+
return payErr("limit must be an integer between 1 and 100", 400);
|
|
1271
|
+
}
|
|
1272
|
+
limit = parsed;
|
|
1273
|
+
}
|
|
1274
|
+
let intents = [...state.boltIntents.values()];
|
|
1275
|
+
if (boltConnectionId) {
|
|
1276
|
+
intents = intents.filter((i) => i.boltConnectionId === boltConnectionId);
|
|
1277
|
+
}
|
|
1278
|
+
if (requestedStatuses && requestedStatuses.length > 0) {
|
|
1279
|
+
const storedWanted = new Set(storedStatusesForFilter(requestedStatuses));
|
|
1280
|
+
intents = intents.filter((i) => storedWanted.has(i.status));
|
|
1281
|
+
}
|
|
1282
|
+
let records = intents.map((i) => toBoltIntentRecord(i, now3));
|
|
1283
|
+
if (requestedStatuses && requestedStatuses.length > 0) {
|
|
1284
|
+
const wanted = new Set(requestedStatuses);
|
|
1285
|
+
records = records.filter((r) => wanted.has(r.status));
|
|
1286
|
+
}
|
|
1287
|
+
records = records.slice(0, limit);
|
|
1288
|
+
return json({ intents: records });
|
|
1289
|
+
}
|
|
1290
|
+
if (intentId && request.method === "DELETE") {
|
|
1291
|
+
const intent = findBoltIntent(intentId);
|
|
1292
|
+
if (!intent) return payErr("Bolt intent not found", 404, "NOT_FOUND");
|
|
1293
|
+
const derived = deriveBoltIntentStatus(intent.status, intent.expiresAt);
|
|
1294
|
+
if (derived !== "PENDING") {
|
|
1295
|
+
return payErr(`Bolt intent is ${derived}, only PENDING intents can be canceled`, 409);
|
|
1296
|
+
}
|
|
1297
|
+
intent.status = "CANCELED";
|
|
1298
|
+
intent.settledAt = /* @__PURE__ */ new Date();
|
|
1299
|
+
return json({ success: true });
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
const chargesMatch = pathname.match(/^\/api\/store\/beam\/([^/]+)\/charges$/);
|
|
1303
|
+
if (chargesMatch && request.method === "GET") {
|
|
1304
|
+
const sourceId = url.searchParams.get("sourceId");
|
|
1305
|
+
if (!sourceId) {
|
|
1306
|
+
return payErr("Missing sourceId parameter", 400);
|
|
1307
|
+
}
|
|
1308
|
+
const charges = state.boltCharges.filter((c) => c.sourceId === sourceId);
|
|
1309
|
+
return json({ charges });
|
|
1310
|
+
}
|
|
1021
1311
|
const beamProductsMatch = pathname.match(
|
|
1022
1312
|
/^\/api\/store\/beam\/([^/]+)\/payment-links(?:\/([^/]+))?$/
|
|
1023
1313
|
);
|