feat(gpc): add auto mode permission checks
This commit is contained in:
@@ -8,6 +8,8 @@
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
|
||||
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
|
||||
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
|
||||
|
||||
function createPlusCheckoutCreateExecutor(deps = {}) {
|
||||
const {
|
||||
@@ -86,6 +88,17 @@
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
function normalizeGpcHelperPhoneMode(value = '') {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.normalizeGpcHelperPhoneMode) {
|
||||
return rootScope.GoPayUtils.normalizeGpcHelperPhoneMode(value);
|
||||
}
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized === GPC_HELPER_PHONE_MODE_AUTO || normalized === 'builtin'
|
||||
? GPC_HELPER_PHONE_MODE_AUTO
|
||||
: GPC_HELPER_PHONE_MODE_MANUAL;
|
||||
}
|
||||
|
||||
function normalizeGpcOtpChannel(value = '') {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.normalizeGpcOtpChannel) {
|
||||
@@ -143,6 +156,17 @@
|
||||
return buildGpcHelperApiUrl(apiUrl, '/api/gp/tasks');
|
||||
}
|
||||
|
||||
function buildGpcBalanceUrl(apiUrl = '') {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.buildGpcApiKeyBalanceUrl) {
|
||||
return rootScope.GoPayUtils.buildGpcApiKeyBalanceUrl(apiUrl);
|
||||
}
|
||||
if (rootScope.GoPayUtils?.buildGpcCardBalanceUrl) {
|
||||
return rootScope.GoPayUtils.buildGpcCardBalanceUrl(apiUrl);
|
||||
}
|
||||
return buildGpcHelperApiUrl(apiUrl, '/api/gp/balance');
|
||||
}
|
||||
|
||||
function unwrapGpcResponse(payload = {}) {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.unwrapGpcResponse) {
|
||||
@@ -176,6 +200,55 @@
|
||||
return payload?.data?.detail || payload?.detail || payload?.message || payload?.error || `HTTP ${status || 0}`;
|
||||
}
|
||||
|
||||
function getGpcRemainingUses(payload = {}) {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.getGpcBalanceRemainingUses) {
|
||||
return rootScope.GoPayUtils.getGpcBalanceRemainingUses(payload);
|
||||
}
|
||||
const data = unwrapGpcResponse(payload);
|
||||
const numeric = Number(data?.remaining_uses ?? data?.remainingUses ?? data?.balance ?? data?.remaining);
|
||||
return Number.isFinite(numeric) ? Math.max(0, Math.floor(numeric)) : null;
|
||||
}
|
||||
|
||||
function isGpcAutoModeEnabled(payload = {}) {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.isGpcAutoModeEnabled) {
|
||||
return rootScope.GoPayUtils.isGpcAutoModeEnabled(payload);
|
||||
}
|
||||
const data = unwrapGpcResponse(payload);
|
||||
return data?.auto_mode_enabled === true || data?.autoModeEnabled === true;
|
||||
}
|
||||
|
||||
async function assertGpcApiKeyReadyForCreate(state = {}, phoneMode = GPC_HELPER_PHONE_MODE_MANUAL, apiKey = '') {
|
||||
const apiUrl = buildGpcBalanceUrl(state?.gopayHelperApiUrl);
|
||||
if (!apiUrl) {
|
||||
throw new Error('创建 GPC 订单失败:缺少 API 地址。');
|
||||
}
|
||||
const { response, data } = await fetchJsonWithTimeout(apiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'X-API-Key': apiKey,
|
||||
},
|
||||
}, 30000);
|
||||
if (!response?.ok || !isGpcUnifiedResponseOk(data)) {
|
||||
const detail = getGpcResponseErrorDetail(data, response?.status || 0);
|
||||
throw new Error(`创建 GPC 订单失败:API Key 校验失败:${detail}`);
|
||||
}
|
||||
const balanceData = unwrapGpcResponse(data);
|
||||
const remainingUses = getGpcRemainingUses(balanceData);
|
||||
const status = String(balanceData?.status || balanceData?.card_status || balanceData?.cardStatus || '').trim().toLowerCase();
|
||||
if (status && status !== 'active') {
|
||||
throw new Error(`创建 GPC 订单失败:API Key 状态不可用(${status})。`);
|
||||
}
|
||||
if (remainingUses !== null && remainingUses <= 0) {
|
||||
throw new Error('创建 GPC 订单失败:API Key 剩余次数不足。');
|
||||
}
|
||||
if (phoneMode === GPC_HELPER_PHONE_MODE_AUTO && !isGpcAutoModeEnabled(balanceData)) {
|
||||
throw new Error('创建 GPC 订单失败:当前 GPC API Key 未开通自动模式。');
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchJsonWithTimeout(url, options = {}, timeoutMs = 30000) {
|
||||
const fetcher = typeof fetchImpl === 'function'
|
||||
? fetchImpl
|
||||
@@ -226,25 +299,31 @@
|
||||
if (!apiUrl) {
|
||||
throw new Error('创建 GPC 订单失败:缺少 API 地址。');
|
||||
}
|
||||
const phoneMode = normalizeGpcHelperPhoneMode(state?.gopayHelperPhoneMode || state?.phoneMode);
|
||||
const isAutoMode = phoneMode === GPC_HELPER_PHONE_MODE_AUTO;
|
||||
const phoneNumber = String(state?.gopayHelperPhoneNumber || '').trim();
|
||||
const countryCode = normalizeHelperCountryCode(state?.gopayHelperCountryCode || '86');
|
||||
const pin = String(state?.gopayHelperPin || '').trim();
|
||||
const apiKey = resolveGpcHelperApiKey(state);
|
||||
if (!phoneNumber) {
|
||||
throw new Error('创建 GPC 订单失败:缺少手机号。');
|
||||
if (!isAutoMode && !phoneNumber) {
|
||||
throw new Error('创建 GPC 订单失败:手动模式缺少手机号。');
|
||||
}
|
||||
if (!pin) {
|
||||
throw new Error('创建 GPC 订单失败:缺少 PIN。');
|
||||
if (!isAutoMode && !pin) {
|
||||
throw new Error('创建 GPC 订单失败:手动模式缺少 PIN。');
|
||||
}
|
||||
|
||||
throwIfStopped();
|
||||
await assertGpcApiKeyReadyForCreate(state, phoneMode, apiKey);
|
||||
throwIfStopped();
|
||||
const payload = {
|
||||
access_token: token,
|
||||
phone_mode: 'manual',
|
||||
country_code: countryCode,
|
||||
phone_number: normalizeHelperPhoneNumber(phoneNumber, countryCode),
|
||||
otp_channel: normalizeGpcOtpChannel(state?.gopayHelperOtpChannel),
|
||||
phone_mode: phoneMode,
|
||||
};
|
||||
if (!isAutoMode) {
|
||||
payload.country_code = countryCode;
|
||||
payload.phone_number = normalizeHelperPhoneNumber(phoneNumber, countryCode);
|
||||
payload.otp_channel = normalizeGpcOtpChannel(state?.gopayHelperOtpChannel);
|
||||
}
|
||||
|
||||
const orderCreatedAt = Date.now();
|
||||
const { response, data } = await fetchJsonWithTimeout(apiUrl, {
|
||||
@@ -273,6 +352,7 @@
|
||||
remoteStage: String(taskData?.remote_stage || taskData?.remoteStage || '').trim(),
|
||||
orderCreatedAt,
|
||||
responsePayload: taskData && typeof taskData === 'object' && !Array.isArray(taskData) ? taskData : null,
|
||||
phoneMode: normalizeGpcHelperPhoneMode(taskData?.phone_mode || taskData?.phoneMode || phoneMode),
|
||||
country: 'ID',
|
||||
currency: 'IDR',
|
||||
checkoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
|
||||
@@ -308,6 +388,7 @@
|
||||
gopayHelperTaskStatus: result.taskStatus,
|
||||
gopayHelperStatusText: result.statusText,
|
||||
gopayHelperRemoteStage: result.remoteStage,
|
||||
gopayHelperPhoneMode: result.phoneMode || normalizeGpcHelperPhoneMode(state?.gopayHelperPhoneMode || state?.phoneMode),
|
||||
gopayHelperTaskPayload: result.responsePayload,
|
||||
gopayHelperReferenceId: '',
|
||||
gopayHelperGoPayGuid: '',
|
||||
@@ -318,7 +399,7 @@
|
||||
gopayHelperStartPayload: null,
|
||||
gopayHelperOrderCreatedAt: result.orderCreatedAt || Date.now(),
|
||||
});
|
||||
await addLog(`步骤 6:GPC 任务已创建(task_id: ${result.taskId}),准备继续下一步。`, 'info');
|
||||
await addLog(`步骤 6:GPC ${result.phoneMode === GPC_HELPER_PHONE_MODE_AUTO ? '自动' : '手动'}模式任务已创建(task_id: ${result.taskId}),准备继续下一步。`, 'info');
|
||||
await completeStepFromBackground(6, {
|
||||
plusCheckoutCountry: result.country || 'ID',
|
||||
plusCheckoutCurrency: result.currency || 'IDR',
|
||||
|
||||
@@ -11,7 +11,27 @@
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
|
||||
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
|
||||
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
|
||||
const GPC_TASK_POLL_INTERVAL_MS = 3000;
|
||||
const GPC_REMOTE_STAGE_LABELS = {
|
||||
auto_otp_wait: '等待自动 OTP',
|
||||
checkout_order_start: '创建订单',
|
||||
checkout_start: '创建订单',
|
||||
completed: '充值完成',
|
||||
gopay_validate_pin: '校验 PIN',
|
||||
otp_ready: '等待 PIN',
|
||||
otp_submitted_local: 'OTP 已提交',
|
||||
payment_processing: '支付处理中',
|
||||
pin_submitted_local: 'PIN 已提交',
|
||||
sms_otp_wait: '等待短信 OTP',
|
||||
whatsapp_otp_wait: '等待 WhatsApp OTP',
|
||||
};
|
||||
const GPC_WAITING_FOR_LABELS = {
|
||||
auto_otp: '自动 OTP',
|
||||
otp: 'OTP',
|
||||
pin: 'PIN',
|
||||
};
|
||||
const PAYMENT_METHOD_CONFIGS = {
|
||||
[PLUS_PAYMENT_METHOD_PAYPAL]: {
|
||||
id: PLUS_PAYMENT_METHOD_PAYPAL,
|
||||
@@ -112,6 +132,56 @@
|
||||
return normalized === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_PAYMENT_METHOD_GOPAY : PLUS_PAYMENT_METHOD_PAYPAL;
|
||||
}
|
||||
|
||||
function normalizeGpcHelperPhoneMode(value = '') {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.normalizeGpcHelperPhoneMode) {
|
||||
return rootScope.GoPayUtils.normalizeGpcHelperPhoneMode(value);
|
||||
}
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized === GPC_HELPER_PHONE_MODE_AUTO || normalized === 'builtin'
|
||||
? GPC_HELPER_PHONE_MODE_AUTO
|
||||
: GPC_HELPER_PHONE_MODE_MANUAL;
|
||||
}
|
||||
|
||||
function formatGpcRemoteStageLabel(stage = '') {
|
||||
const normalized = String(stage || '').trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return '';
|
||||
}
|
||||
return GPC_REMOTE_STAGE_LABELS[normalized] || normalized;
|
||||
}
|
||||
|
||||
function formatGpcWaitingForLabel(waitingFor = '') {
|
||||
const normalized = String(waitingFor || '').trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return '';
|
||||
}
|
||||
return GPC_WAITING_FOR_LABELS[normalized] || normalized.toUpperCase();
|
||||
}
|
||||
|
||||
function formatGpcTaskStatusLog(task = {}) {
|
||||
const statusText = String(task?.status_text || task?.statusText || '').trim();
|
||||
const status = String(task?.status || '').trim();
|
||||
const remoteStage = String(task?.remote_stage || task?.remoteStage || '').trim();
|
||||
const stageText = formatGpcRemoteStageLabel(remoteStage);
|
||||
const waitingForText = formatGpcWaitingForLabel(task?.api_waiting_for || task?.apiWaitingFor || '');
|
||||
const mainText = stageText || statusText || status || '处理中';
|
||||
const parts = [`步骤 7:GPC 任务状态:${mainText}`];
|
||||
if (waitingForText && !mainText.includes(waitingForText)) {
|
||||
parts.push(`,等待 ${waitingForText}`);
|
||||
}
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
function getGpcHelperPhoneMode(state = {}, task = null) {
|
||||
return normalizeGpcHelperPhoneMode(
|
||||
task?.phone_mode
|
||||
|| task?.phoneMode
|
||||
|| state?.gopayHelperPhoneMode
|
||||
|| state?.phoneMode
|
||||
);
|
||||
}
|
||||
|
||||
function getPaymentMethodConfig(method = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||||
return PAYMENT_METHOD_CONFIGS[normalizePlusPaymentMethod(method)] || PAYMENT_METHOD_CONFIGS[PLUS_PAYMENT_METHOD_PAYPAL];
|
||||
}
|
||||
@@ -299,7 +369,7 @@
|
||||
task.task_id = String(task.task_id || task.taskId || '').trim();
|
||||
task.status = String(task.status || '').trim().toLowerCase();
|
||||
task.status_text = String(task.status_text || task.statusText || '').trim();
|
||||
task.phone_mode = String(task.phone_mode || task.phoneMode || '').trim().toLowerCase();
|
||||
task.phone_mode = normalizeGpcHelperPhoneMode(task.phone_mode || task.phoneMode || '');
|
||||
task.remote_stage = String(task.remote_stage || task.remoteStage || '').trim().toLowerCase();
|
||||
task.api_waiting_for = String(task.api_waiting_for || task.apiWaitingFor || '').trim().toLowerCase();
|
||||
task.api_input_deadline_at = String(task.api_input_deadline_at || task.apiInputDeadlineAt || '').trim();
|
||||
@@ -318,6 +388,7 @@
|
||||
gopayHelperTaskId: task.task_id,
|
||||
gopayHelperTaskStatus: task.status,
|
||||
gopayHelperStatusText: task.status_text,
|
||||
gopayHelperPhoneMode: task.phone_mode,
|
||||
gopayHelperRemoteStage: task.remote_stage,
|
||||
gopayHelperApiWaitingFor: task.api_waiting_for,
|
||||
gopayHelperApiInputDeadlineAt: task.api_input_deadline_at,
|
||||
@@ -675,12 +746,16 @@
|
||||
});
|
||||
}
|
||||
|
||||
function isGpcTaskOtpWait(task = {}) {
|
||||
return task?.phone_mode === 'manual' && task?.api_waiting_for === 'otp';
|
||||
function isGpcTaskManualMode(task = {}, state = {}) {
|
||||
return getGpcHelperPhoneMode(state, task) === GPC_HELPER_PHONE_MODE_MANUAL;
|
||||
}
|
||||
|
||||
function isGpcTaskPinWait(task = {}) {
|
||||
return task?.phone_mode === 'manual'
|
||||
function isGpcTaskOtpWait(task = {}, state = {}) {
|
||||
return isGpcTaskManualMode(task, state) && task?.api_waiting_for === 'otp';
|
||||
}
|
||||
|
||||
function isGpcTaskPinWait(task = {}, state = {}) {
|
||||
return isGpcTaskManualMode(task, state)
|
||||
&& (task?.api_waiting_for === 'pin' || task?.status === 'otp_ready');
|
||||
}
|
||||
|
||||
@@ -840,27 +915,25 @@
|
||||
throw new Error('步骤 7:GPC 模式缺少 API Key。');
|
||||
}
|
||||
|
||||
const configuredPhoneMode = normalizeGpcHelperPhoneMode(state?.gopayHelperPhoneMode || state?.phoneMode || GPC_HELPER_PHONE_MODE_MANUAL);
|
||||
const rawPin = String(state?.gopayHelperPin || '').trim();
|
||||
const pinDigits = rawPin.replace(/[^\d]/g, '');
|
||||
const pin = normalizeSixDigitPin(rawPin);
|
||||
if (!pin) {
|
||||
if (configuredPhoneMode === GPC_HELPER_PHONE_MODE_MANUAL && !pin) {
|
||||
if (taskId && apiUrl && apiKey) {
|
||||
await stopGpcTaskBestEffort(apiUrl, taskId, apiKey, 'PIN 配置错误');
|
||||
}
|
||||
throw new Error(pinDigits
|
||||
? '步骤 7:GPC PIN 必须是 6 位数字,请检查侧边栏配置。'
|
||||
: '步骤 7:GPC 模式缺少 PIN 配置。');
|
||||
: '步骤 7:GPC 手动模式缺少 PIN 配置。');
|
||||
}
|
||||
|
||||
await addLog(`步骤 7:GPC 模式开始轮询任务(task_id: ${taskId})...`, 'info');
|
||||
await addLog(`步骤 7:GPC ${configuredPhoneMode === GPC_HELPER_PHONE_MODE_AUTO ? '自动' : '手动'}模式开始轮询任务(task_id: ${taskId})...`, 'info');
|
||||
try {
|
||||
while (Date.now() <= deadline) {
|
||||
throwIfStopped();
|
||||
const task = await fetchGpcTaskStatus(apiUrl, taskId, apiKey);
|
||||
const statusText = task?.status_text || task?.status || '处理中';
|
||||
const remoteStage = task?.remote_stage || '';
|
||||
const waitingFor = task?.api_waiting_for || '';
|
||||
await addLog(`步骤 7:GPC 任务状态:${statusText}${remoteStage ? `(${remoteStage})` : ''}${waitingFor ? `,等待 ${waitingFor.toUpperCase()}` : ''}`, 'info');
|
||||
await addLog(formatGpcTaskStatusLog(task), 'info');
|
||||
|
||||
if (task.status === 'completed') {
|
||||
terminalReached = true;
|
||||
@@ -879,7 +952,7 @@
|
||||
throw buildGpcTaskEndedError(task, 'GPC 任务已结束,请重新创建任务。');
|
||||
}
|
||||
|
||||
if (isGpcTaskOtpWait(task)) {
|
||||
if (isGpcTaskOtpWait(task, state)) {
|
||||
if (isGpcTaskInputDeadlineExpired(task)) {
|
||||
throw buildGpcInputDeadlineError(task, 'OTP');
|
||||
}
|
||||
@@ -940,7 +1013,7 @@
|
||||
otpLastSubmittedAt = Date.now();
|
||||
lastSubmittedOtp = normalizedOtp;
|
||||
await addLog('步骤 7:OTP 已提交,继续等待 GPC 任务状态更新。', 'ok');
|
||||
} else if (isGpcTaskPinWait(task) && !pinSubmitted) {
|
||||
} else if (isGpcTaskPinWait(task, state) && !pinSubmitted) {
|
||||
if (isGpcTaskInputDeadlineExpired(task)) {
|
||||
throw buildGpcInputDeadlineError(task, 'PIN');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user