移除快手小店以及 api
This commit is contained in:
@@ -6,7 +6,6 @@ import {
|
||||
isPlainObject,
|
||||
pickFirstNonEmpty,
|
||||
resolveCloudtentaclesAdminContext,
|
||||
resolveKuaishouEticketAdminContext,
|
||||
} from './context.js'
|
||||
|
||||
test('pickFirstNonEmpty returns first trimmed non-empty string value', () => {
|
||||
@@ -21,52 +20,6 @@ test('isPlainObject only accepts non-array objects', () => {
|
||||
assert.equal(isPlainObject('x'), false)
|
||||
})
|
||||
|
||||
test('resolveKuaishouEticketAdminContext prefers explicit payload values', () => {
|
||||
const context = resolveKuaishouEticketAdminContext(
|
||||
{
|
||||
baseUrl: ' https://override.example.com ',
|
||||
shopId: ' shop-2 ',
|
||||
cookie: ' cookie-2 ',
|
||||
},
|
||||
{
|
||||
savedSource: { baseUrl: 'https://saved.example.com' },
|
||||
findShopConfig(requestedShopId) {
|
||||
assert.equal(requestedShopId, 'shop-2')
|
||||
return { shopId: 'shop-2', cookie: 'saved-cookie-2' }
|
||||
},
|
||||
getFirstAvailableShop() {
|
||||
throw new Error('should not use first available shop')
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert.deepEqual(context, {
|
||||
baseUrl: 'https://override.example.com',
|
||||
cookie: 'cookie-2',
|
||||
shopId: 'shop-2',
|
||||
shop: { shopId: 'shop-2', cookie: 'saved-cookie-2' },
|
||||
})
|
||||
})
|
||||
|
||||
test('resolveKuaishouEticketAdminContext falls back to first available shop', () => {
|
||||
const context = resolveKuaishouEticketAdminContext(
|
||||
{},
|
||||
{
|
||||
savedSource: { baseUrl: ' https://saved.example.com ' },
|
||||
getFirstAvailableShop() {
|
||||
return { shopId: 'shop-1', cookie: 'cookie-1' }
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert.deepEqual(context, {
|
||||
baseUrl: 'https://saved.example.com',
|
||||
cookie: 'cookie-1',
|
||||
shopId: 'shop-1',
|
||||
shop: { shopId: 'shop-1', cookie: 'cookie-1' },
|
||||
})
|
||||
})
|
||||
|
||||
test('resolveCloudtentaclesAdminContext merges payload source and persisted session', () => {
|
||||
const context = resolveCloudtentaclesAdminContext(
|
||||
{
|
||||
|
||||
@@ -15,32 +15,6 @@ export function isPlainObject(value: unknown): value is JsonObject {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export function resolveKuaishouEticketAdminContext(payload: JsonObject = {}, options: JsonObject = {}) {
|
||||
const savedSource = options.savedSource || {};
|
||||
const findShopConfig =
|
||||
(options.findShopConfig || (() => null)) as (shopId: string, source: JsonObject) => unknown;
|
||||
const getFirstAvailableShop =
|
||||
(options.getFirstAvailableShop || (() => null)) as (source: JsonObject) => unknown;
|
||||
const defaultBaseUrl = String(
|
||||
options.defaultBaseUrl || "https://s.kwaixiaodian.com"
|
||||
).trim();
|
||||
const requestedShopId = String(payload.shopId || "").trim();
|
||||
const configuredShop = (requestedShopId
|
||||
? findShopConfig(requestedShopId, savedSource)
|
||||
: getFirstAvailableShop(savedSource)) as JsonObject | null;
|
||||
|
||||
return {
|
||||
baseUrl: pickFirstNonEmpty([
|
||||
payload.baseUrl,
|
||||
savedSource.baseUrl,
|
||||
defaultBaseUrl,
|
||||
]),
|
||||
cookie: pickFirstNonEmpty([payload.cookie, configuredShop?.cookie]),
|
||||
shopId: pickFirstNonEmpty([requestedShopId, configuredShop?.shopId]),
|
||||
shop: configuredShop,
|
||||
};
|
||||
}
|
||||
|
||||
import { getCloudtentaclesSourceByKey } from "../../../platforms/cloudtentacles/source-config-service.js";
|
||||
import { getCloudtentaclesSessionStateByKey } from "../../../platforms/cloudtentacles/session-state-service.js";
|
||||
import {
|
||||
|
||||
@@ -3,7 +3,6 @@ import assert from 'node:assert/strict'
|
||||
|
||||
import {
|
||||
mapAdminCloudtentaclesSession,
|
||||
mapAdminKuaishouEticketSourceConfig,
|
||||
maskPhone,
|
||||
maskSecret,
|
||||
} from './mappers.js'
|
||||
@@ -20,30 +19,6 @@ test('maskPhone hides middle digits for mobile numbers', () => {
|
||||
assert.equal(maskPhone(''), '')
|
||||
})
|
||||
|
||||
test('mapAdminKuaishouEticketSourceConfig maps shops and masks cookies', () => {
|
||||
assert.deepEqual(
|
||||
mapAdminKuaishouEticketSourceConfig(
|
||||
{ enabled: true, baseUrl: ' https://s.kwaixiaodian.com ' },
|
||||
[{ shopId: ' 1 ', kshopName: ' A店 ', cookie: 'cookie-abcdef123456', enabled: true }],
|
||||
),
|
||||
{
|
||||
enabled: true,
|
||||
baseUrl: 'https://s.kwaixiaodian.com',
|
||||
shops: [
|
||||
{
|
||||
shopId: '1',
|
||||
kshopName: 'A店',
|
||||
cookie: 'cookie-abcdef123456',
|
||||
cookieMasked: 'cookie****123456',
|
||||
hasCookie: true,
|
||||
userAvatar: '',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('mapAdminCloudtentaclesSession derives masked fields and token presence', () => {
|
||||
assert.deepEqual(
|
||||
mapAdminCloudtentaclesSession({
|
||||
|
||||
@@ -17,26 +17,6 @@ export function maskPhone(value: unknown) {
|
||||
return maskPhoneValue(value, { maskShort: false });
|
||||
}
|
||||
|
||||
export function mapAdminKuaishouEticketShopItem(item: JsonObject = {}) {
|
||||
return {
|
||||
shopId: String(item.shopId || "").trim(),
|
||||
kshopName: String(item.kshopName || "").trim(),
|
||||
cookie: String(item.cookie || "").trim(),
|
||||
cookieMasked: maskSecret(item.cookie),
|
||||
hasCookie: Boolean(String(item.cookie || "").trim()),
|
||||
userAvatar: String(item.userAvatar || "").trim(),
|
||||
enabled: item.enabled !== false,
|
||||
};
|
||||
}
|
||||
|
||||
export function mapAdminKuaishouEticketSourceConfig(config: JsonObject = {}, shops: JsonObject[] = []) {
|
||||
return {
|
||||
enabled: config.enabled !== false,
|
||||
baseUrl: String(config.baseUrl || "").trim(),
|
||||
shops: shops.map((item) => mapAdminKuaishouEticketShopItem(item)),
|
||||
};
|
||||
}
|
||||
|
||||
export function mapAdminCloudtentaclesSourceConfig(config: JsonObject = {}) {
|
||||
return {
|
||||
key: String(config.key || "").trim(),
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import {
|
||||
consumeKuaishouEticket,
|
||||
queryKuaishouEticketConsumeDetail,
|
||||
} from '../../platforms/kuaishou-eticket/consume-service.js'
|
||||
import {
|
||||
findKuaishouEticketShopConfig,
|
||||
getFirstAvailableKuaishouEticketShop,
|
||||
getKuaishouEticketSourceConfig,
|
||||
getKuaishouEticketSourceFilePath,
|
||||
listKuaishouEticketShopConfigs,
|
||||
saveKuaishouEticketSourceConfig,
|
||||
} from '../../platforms/kuaishou-eticket/source-config-service.js'
|
||||
import { queryKuaishouEticketStableInfo } from '../../platforms/kuaishou-eticket/info-service.js'
|
||||
import { resolveKuaishouEticketAdminContext } from './cloudtentacles/context.js'
|
||||
import { mapAdminKuaishouEticketSourceConfig } from './cloudtentacles/mappers.js'
|
||||
|
||||
type JsonObject = Record<string, any>
|
||||
|
||||
function resolveAdminKuaishouEticketContext(payload: JsonObject = {}) {
|
||||
return resolveKuaishouEticketAdminContext(payload, {
|
||||
savedSource: getKuaishouEticketSourceConfig(),
|
||||
findShopConfig: findKuaishouEticketShopConfig,
|
||||
getFirstAvailableShop: getFirstAvailableKuaishouEticketShop,
|
||||
})
|
||||
}
|
||||
|
||||
export function getAdminKuaishouEticketSourceConfig() {
|
||||
const config = getKuaishouEticketSourceConfig()
|
||||
|
||||
return {
|
||||
filePath: getKuaishouEticketSourceFilePath(),
|
||||
source: mapAdminKuaishouEticketSourceConfig(config, listKuaishouEticketShopConfigs(config)),
|
||||
}
|
||||
}
|
||||
|
||||
export function updateAdminKuaishouEticketSourceConfig(payload: JsonObject = {}) {
|
||||
const saved = saveKuaishouEticketSourceConfig({
|
||||
enabled: payload.enabled !== false,
|
||||
baseUrl: String(payload.baseUrl || '').trim() || 'https://s.kwaixiaodian.com',
|
||||
shops: Array.isArray(payload.shops) ? payload.shops : [],
|
||||
})
|
||||
|
||||
return {
|
||||
filePath: getKuaishouEticketSourceFilePath(),
|
||||
source: mapAdminKuaishouEticketSourceConfig(saved, listKuaishouEticketShopConfigs(saved)),
|
||||
}
|
||||
}
|
||||
|
||||
export async function queryAdminKuaishouEticketDetail(payload: JsonObject = {}) {
|
||||
const context = resolveAdminKuaishouEticketContext(payload)
|
||||
|
||||
return queryKuaishouEticketConsumeDetail({
|
||||
baseUrl: context.baseUrl,
|
||||
cookie: context.cookie,
|
||||
eTicketId: String(payload.eTicketId || '').trim(),
|
||||
})
|
||||
}
|
||||
|
||||
export async function queryAdminKuaishouEticketShopInfo(payload: JsonObject = {}) {
|
||||
const context = resolveAdminKuaishouEticketContext(payload)
|
||||
|
||||
return queryKuaishouEticketStableInfo({
|
||||
baseUrl: context.baseUrl,
|
||||
cookie: context.cookie,
|
||||
})
|
||||
}
|
||||
|
||||
export async function consumeAdminKuaishouEticket(payload: JsonObject = {}) {
|
||||
const context = resolveAdminKuaishouEticketContext(payload)
|
||||
|
||||
return consumeKuaishouEticket({
|
||||
baseUrl: context.baseUrl,
|
||||
cookie: context.cookie,
|
||||
eTicketId: String(payload.eTicketId || '').trim(),
|
||||
oid: String(payload.oid || '').trim(),
|
||||
formToken: String(payload.formToken || '').trim(),
|
||||
num: payload.num,
|
||||
storeId: String(payload.storeId || '').trim(),
|
||||
})
|
||||
}
|
||||
@@ -17,18 +17,12 @@ import {
|
||||
backCloudtentaclesVirtualNumber,
|
||||
getCloudtentaclesBindInfo,
|
||||
} from '../../platforms/cloudtentacles/virtual-number-service.js'
|
||||
import { consumeKuaishouEticket } from '../../platforms/kuaishou-eticket/consume-service.js'
|
||||
import { isKuaishouEticketMockTicketCode } from '../../platforms/kuaishou-eticket/mock-ticket-service.js'
|
||||
import { resendKuaishouIndustryVoucherSendCallback } from '../../platforms/kuaishou-industry/send-code-service.js'
|
||||
import { consumeKuaishouIndustryVoucher } from '../../platforms/kuaishou-industry/voucher-service.js'
|
||||
import {
|
||||
attachKuaishouIndustryVoucherToTask,
|
||||
bindKuaishouIndustryVouchersToOrderTasks,
|
||||
} from '../../platforms/kuaishou-industry/voucher-binding-service.js'
|
||||
import {
|
||||
getKuaishouEticketSourceConfig,
|
||||
resolveKuaishouEticketShopConfig,
|
||||
} from '../../platforms/kuaishou-eticket/source-config-service.js'
|
||||
import { createHttpError } from '../../../utils/http.js'
|
||||
import { nowIso } from '../../../utils/time.js'
|
||||
import { TASK_STATUS } from '../../../domain/task-status.js'
|
||||
@@ -58,7 +52,6 @@ import type {
|
||||
AdminViewerSessionInput,
|
||||
} from '../../../types/admin/read-inputs.js'
|
||||
import type {
|
||||
AdminTaskKuaishouCloudDispatchInput,
|
||||
AdminTaskKuaishouIndustryConsumeInput,
|
||||
} from '../../../types/admin/write-inputs.js'
|
||||
import type { AdminTaskActionResponse } from '../../../types/admin/write-models.js'
|
||||
@@ -246,7 +239,6 @@ export async function prepareAdminTaskKuaishouCloudFulfillment(
|
||||
|
||||
export async function dispatchAdminTaskKuaishouCloudFulfillment(
|
||||
taskId: AdminEntityIdInput,
|
||||
payload: AdminTaskKuaishouCloudDispatchInput = {},
|
||||
session: AdminViewerSessionInput | null = null,
|
||||
): Promise<AdminTaskActionResponse> {
|
||||
const task = await getRequiredTask(taskId)
|
||||
@@ -281,11 +273,15 @@ export async function dispatchAdminTaskKuaishouCloudFulfillment(
|
||||
})
|
||||
}
|
||||
|
||||
const ticketCode = String(payload.ticketCode || '').trim()
|
||||
const persistedTicketCode = String(flow.ticket.code || '').trim()
|
||||
const voucherContext = normalizeAdminTaskIndustryVoucherContext(taskContext.kuaishouIndustryVoucher)
|
||||
const industryVoucherCode = String(
|
||||
voucherContext.voucherCode || voucherContext.eticketId || '',
|
||||
).trim()
|
||||
const resolvedTicketCode = persistedTicketCode || industryVoucherCode
|
||||
|
||||
if (!persistedTicketCode && !ticketCode) {
|
||||
throw createHttpError('客户还没有在领取页提交核销码,暂时不能直接发货', {
|
||||
if (!resolvedTicketCode) {
|
||||
throw createHttpError('旧快手小店核销流程已停用,请改用行业电子凭证处理', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_task_kuaishou_cloud_missing_ticket_code',
|
||||
})
|
||||
@@ -322,16 +318,9 @@ export async function dispatchAdminTaskKuaishouCloudFulfillment(
|
||||
...syncedFlow,
|
||||
ticket: {
|
||||
...syncedFlow.ticket,
|
||||
code: ticketCode || persistedTicketCode,
|
||||
capturedAt: ticketCode ? now : syncedFlow.ticket.capturedAt,
|
||||
capturedBy:
|
||||
ticketCode && session
|
||||
? {
|
||||
userId: Number(session.userId || 0) || 0,
|
||||
username: String(session.username || '').trim(),
|
||||
role: String(session.role || '').trim(),
|
||||
}
|
||||
: syncedFlow.ticket.capturedBy,
|
||||
code: resolvedTicketCode,
|
||||
capturedAt: resolvedTicketCode ? (syncedFlow.ticket.capturedAt || now) : syncedFlow.ticket.capturedAt,
|
||||
capturedBy: syncedFlow.ticket.capturedBy,
|
||||
},
|
||||
dispatch: {
|
||||
...syncedFlow.dispatch,
|
||||
@@ -366,7 +355,7 @@ export async function dispatchAdminTaskKuaishouCloudFulfillment(
|
||||
task.id,
|
||||
'kuaishou_cloud_dispatched',
|
||||
{
|
||||
ticketCodeMasked: maskCode(ticketCode || persistedTicketCode),
|
||||
ticketCodeMasked: maskCode(resolvedTicketCode),
|
||||
skuId: syncedFlow.binding.skuId,
|
||||
vnId: syncedFlow.binding.vnId,
|
||||
vnPhoneMasked: maskPhone(syncedFlow.binding.vnPhone),
|
||||
@@ -548,77 +537,18 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment(
|
||||
id: flow.binding.vnId,
|
||||
})
|
||||
|
||||
const order = await getOrderById(task.order_id)
|
||||
const ticketCode = String(flow.ticket.code || '').trim()
|
||||
const shopId = String(flow.consume.shopId || order?.shop_id || '').trim()
|
||||
const shopName = String(flow.consume.shopName || order?.shop_name || '').trim()
|
||||
const mockTicketCode = isKuaishouEticketMockTicketCode(ticketCode)
|
||||
const eticketSource = getKuaishouEticketSourceConfig()
|
||||
const shopConfig = resolveKuaishouEticketShopConfig({
|
||||
shopId,
|
||||
shopName,
|
||||
})
|
||||
let consumeStatus = 'pending'
|
||||
let consumeErrorMessage = ''
|
||||
let consumedAt = null
|
||||
let nextTaskStatus = 'completed'
|
||||
let nextResultCode = 'kuaishou_cloud_completed'
|
||||
let nextResultMessage = 'cloudtentacles 发货、退号并完成快手核销'
|
||||
const consumeAlreadyCompleted = flow.consume.status === 'success'
|
||||
|
||||
if (consumeAlreadyCompleted) {
|
||||
consumeStatus = 'success'
|
||||
consumedAt = flow.consume.consumedAt || now
|
||||
} else if (!order) {
|
||||
consumeStatus = 'failed'
|
||||
consumeErrorMessage = '任务关联订单不存在,无法执行快手核销'
|
||||
} else if (!ticketCode) {
|
||||
consumeStatus = 'failed'
|
||||
consumeErrorMessage = '客户未提交有效核销码,无法执行快手核销'
|
||||
} else if (mockTicketCode) {
|
||||
const consumeResult = await consumeKuaishouEticket({
|
||||
eTicketId: ticketCode,
|
||||
oid: String(flow.ticket.oid || '').trim(),
|
||||
formToken: String(flow.ticket.formToken || '').trim(),
|
||||
})
|
||||
consumeStatus = consumeResult.consumed ? 'success' : 'failed'
|
||||
consumedAt = consumeResult.consumed ? now : null
|
||||
consumeErrorMessage = String(consumeResult.errorMessage || '').trim()
|
||||
} else if (
|
||||
!shopConfig ||
|
||||
shopConfig.enabled === false ||
|
||||
!String(shopConfig.cookie || '').trim()
|
||||
) {
|
||||
consumeStatus = 'failed'
|
||||
consumeErrorMessage = '订单对应快手小店缺少可用 Cookie,无法执行快手核销'
|
||||
} else {
|
||||
try {
|
||||
const consumeResult = await consumeKuaishouEticket({
|
||||
baseUrl: eticketSource.baseUrl,
|
||||
cookie: shopConfig.cookie,
|
||||
eTicketId: ticketCode,
|
||||
oid: String(flow.ticket.oid || '').trim(),
|
||||
formToken: String(flow.ticket.formToken || '').trim(),
|
||||
})
|
||||
|
||||
if (consumeResult.consumed) {
|
||||
consumeStatus = 'success'
|
||||
consumedAt = now
|
||||
} else {
|
||||
consumeStatus = 'failed'
|
||||
consumeErrorMessage = String(consumeResult.errorMessage || '快手核销失败').trim()
|
||||
}
|
||||
} catch (error) {
|
||||
consumeStatus = 'failed'
|
||||
consumeErrorMessage = error instanceof Error ? error.message : '快手核销失败'
|
||||
}
|
||||
}
|
||||
|
||||
if (consumeStatus !== 'success') {
|
||||
nextTaskStatus = 'manual_review'
|
||||
nextResultCode = 'kuaishou_cloud_consume_failed'
|
||||
nextResultMessage = consumeErrorMessage || '号码已退还,但快手核销未完成,请人工处理'
|
||||
}
|
||||
const consumeStatus = consumeAlreadyCompleted ? 'success' : 'skipped'
|
||||
let consumeErrorMessage = ''
|
||||
const consumedAt = consumeAlreadyCompleted ? flow.consume.consumedAt || now : null
|
||||
const nextTaskStatus = 'completed'
|
||||
const nextResultCode = consumeAlreadyCompleted
|
||||
? 'kuaishou_cloud_completed'
|
||||
: 'kuaishou_cloud_completed_without_eticket_consume'
|
||||
const nextResultMessage = consumeAlreadyCompleted
|
||||
? 'cloudtentacles 发货、退号并完成电子凭证核销'
|
||||
: 'cloudtentacles 发货、退号并收口'
|
||||
|
||||
const nextContext = {
|
||||
...taskContext,
|
||||
@@ -639,8 +569,8 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment(
|
||||
consume: {
|
||||
...flow.consume,
|
||||
status: consumeStatus,
|
||||
shopId: shopId || flow.consume.shopId,
|
||||
shopName,
|
||||
shopId: flow.consume.shopId,
|
||||
shopName: flow.consume.shopName,
|
||||
autoConsumeEnabled: flow.consume.autoConsumeEnabled === true,
|
||||
consumedAt,
|
||||
errorMessage: consumeErrorMessage,
|
||||
@@ -653,7 +583,7 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment(
|
||||
delivery_status: 'delivered',
|
||||
result_code: nextResultCode,
|
||||
result_message: nextResultMessage,
|
||||
redeemed_at: consumeStatus === 'success' ? now : task.redeemed_at,
|
||||
redeemed_at: now,
|
||||
last_error: consumeErrorMessage,
|
||||
context_json: JSON.stringify(nextContext),
|
||||
updated_at: now,
|
||||
@@ -669,18 +599,19 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment(
|
||||
now,
|
||||
)
|
||||
|
||||
const consumeEventType = consumeAlreadyCompleted
|
||||
? 'kuaishou_cloud_consume_already_completed'
|
||||
: 'kuaishou_cloud_consume_skipped'
|
||||
|
||||
await createTaskEvent(
|
||||
task.id,
|
||||
consumeAlreadyCompleted
|
||||
? 'kuaishou_cloud_consume_already_completed'
|
||||
: consumeStatus === 'success'
|
||||
? 'kuaishou_cloud_consumed'
|
||||
: 'kuaishou_cloud_consume_failed',
|
||||
consumeEventType,
|
||||
{
|
||||
ticketCodeMasked: maskCode(ticketCode),
|
||||
shopId,
|
||||
shopName,
|
||||
shopId: flow.consume.shopId,
|
||||
shopName: flow.consume.shopName,
|
||||
consumeStatus,
|
||||
consumeMode: 'legacy_writeoff_disabled',
|
||||
errorMessage: consumeErrorMessage,
|
||||
},
|
||||
now,
|
||||
@@ -961,7 +892,7 @@ async function getRequiredIndustryVoucherForTask(task: TaskRow): Promise<Kuaisho
|
||||
}
|
||||
|
||||
const taskContext = parseTaskContext(task)
|
||||
const voucherContext = taskContext.kuaishouIndustryVoucher || {}
|
||||
const voucherContext = normalizeAdminTaskIndustryVoucherContext(taskContext.kuaishouIndustryVoucher)
|
||||
const voucherCode = String(voucherContext.voucherCode || voucherContext.eticketId || '').trim()
|
||||
if (voucherCode) {
|
||||
const voucher = await findKuaishouIndustryVoucherByCode(voucherCode, task.platform_order_id)
|
||||
@@ -1000,6 +931,12 @@ async function getRequiredIndustryVoucherForTask(task: TaskRow): Promise<Kuaisho
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeAdminTaskIndustryVoucherContext(value: unknown): Record<string, any> {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? value as Record<string, any>
|
||||
: {}
|
||||
}
|
||||
|
||||
async function resolveIndustryVouchersForOrder(platformOrderId: string): Promise<KuaishouIndustryVoucherRow[]> {
|
||||
const normalizedOid = String(platformOrderId || '').trim()
|
||||
if (!normalizedOid) {
|
||||
|
||||
Reference in New Issue
Block a user