feat: add inbound hold lifecycle
This commit is contained in:
@@ -5,7 +5,7 @@ import { dirname, resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import process from 'node:process'
|
||||
|
||||
const CONFIGURED_MODULES = ['merchant-qualification-fixture', 'customers', 'overseas-storage-fees', 'system', 'notifications', 'master-data', 'suppliers', 'domestic-container', 'orders', 'fee-items', 'pricing', 'downloads', 'inbound-weight-ocr', 'inbound-measurement', 'inbound-fees', 'phase3-container-loading', 'phase3-container-overview', 'phase3-loading', 'phase3-loading-model-bridge', 'phase3-pda-convergence', 'phase3-evidence-gate', 'supplemental', 'supplemental-boundaries', 'ocean-bookings', 'truck-appointments', 'service-center-flow', 'phase3-loading-finalize', 'phase3-container-documents', 'phase3-customs', 'phase3-container-customs', 'phase3-port-schedules', 'phase3-carrier-schedule-queries', 'phase3-outbound-dispatch', 'phase3-outbound-aggregate', 'phase3-dispatch-gate', 'phase3-waiting-for-loading', 'phase3-warehouse-rbac', 'phase3-order-splitting', 'manual-state-matrix', 'phase4-overseas-warehouse', 'phase4-carrier-pickup-ready', 'phase4-overseas-pda', 'phase4-overseas-pda-operations', 'phase4-overseas-inbound-operations', 'phase4-overseas-client-services', 'phase4-overseas-dispatch', 'phase5-overseas-fulfillment', 'phase4-cross-region-events', 'phase4-overseas-business', 'phase4-overseas-master-data', 'phase4-carrier-nodes', 'finance-full-fixtures', 'finance-reconciliation-full', 'finance-settlement-full', 'finance-direct-paid-isolation', 'finance-downloads', 'firstleg-container-fee-allocation', 'phase4-overseas-finance-cross-region', 'phase4-overseas-wallet-and-pallet-cost', 'customer-operating-statistics']
|
||||
const CONFIGURED_MODULES = ['merchant-qualification-fixture', 'customers', 'overseas-storage-fees', 'system', 'notifications', 'master-data', 'suppliers', 'domestic-container', 'orders', 'inbound-hold', 'fee-items', 'pricing', 'downloads', 'inbound-weight-ocr', 'inbound-measurement', 'inbound-fees', 'phase3-container-loading', 'phase3-container-overview', 'phase3-loading', 'phase3-loading-model-bridge', 'phase3-pda-convergence', 'phase3-evidence-gate', 'supplemental', 'supplemental-boundaries', 'ocean-bookings', 'truck-appointments', 'service-center-flow', 'phase3-loading-finalize', 'phase3-container-documents', 'phase3-customs', 'phase3-container-customs', 'phase3-port-schedules', 'phase3-carrier-schedule-queries', 'phase3-outbound-dispatch', 'phase3-outbound-aggregate', 'phase3-dispatch-gate', 'phase3-waiting-for-loading', 'phase3-warehouse-rbac', 'phase3-order-splitting', 'manual-state-matrix', 'phase4-overseas-warehouse', 'phase4-carrier-pickup-ready', 'phase4-overseas-pda', 'phase4-overseas-pda-operations', 'phase4-overseas-inbound-operations', 'phase4-overseas-client-services', 'phase4-overseas-dispatch', 'phase5-overseas-fulfillment', 'phase4-cross-region-events', 'phase4-overseas-business', 'phase4-overseas-master-data', 'phase4-carrier-nodes', 'finance-full-fixtures', 'finance-reconciliation-full', 'finance-settlement-full', 'finance-direct-paid-isolation', 'finance-downloads', 'firstleg-container-fee-allocation', 'phase4-overseas-finance-cross-region', 'phase4-overseas-wallet-and-pallet-cost', 'customer-operating-statistics']
|
||||
const OPTIONAL_MODULES = ['manual-order-splitting-fixture', 'manual-order-splitting-verify',
|
||||
'manual-preload-push-fixture', 'manual-preload-push-verify',
|
||||
'manual-pda-ready-fixture', 'manual-pda-ready-verify',
|
||||
@@ -45,6 +45,7 @@ const MODULE_DEPENDENCIES = new Map([
|
||||
['suppliers', ['merchant-qualification-fixture']],
|
||||
['supplemental', ['merchant-qualification-fixture']],
|
||||
['orders', ['merchant-qualification-fixture']],
|
||||
['inbound-hold', ['orders']],
|
||||
['inbound-measurement', ['orders']],
|
||||
['manual-order-splitting-fixture', ['inbound-measurement']],
|
||||
['coload-groups', ['inbound-measurement']],
|
||||
|
||||
@@ -62,6 +62,7 @@ export async function run(context) {
|
||||
| 公共主数据与往来单位 | `master-data.mjs` | `master-data-partner-contract.test.mjs` |
|
||||
| 平台基础、客户、海外仓储费与下载 | `system.mjs`、`customers.mjs`、`overseas-storage-fees.mjs`、`downloads.mjs` | 由 Admin 总入口默认编排;分别覆盖系统能力、客户管理、海外仓阶梯费和下载中心基础能力 |
|
||||
| 订单创建、可计价主数据、幂等零漂移与审核异步收敛 | `orders.mjs` | `orders-pricing-fixture-contract.test.mjs`、`orders-lifecycle-idempotency-contract.test.mjs`、`orders-approval-convergence-contract.test.mjs` |
|
||||
| 入库单手工扣货与解除 | `inbound-hold.mjs`(客户隔离、乐观锁、幂等重放/冲突、不可变历史、统一订单投影、智能装柜候选拦截与恢复) | `inbound-hold-contract.test.mjs`;默认全量已启用,定向入口自动展开已审核订单与入库单造数 |
|
||||
| 费用项目、价格配置与基础 Finance | `fee-items.mjs`、`pricing.mjs`、`finance.mjs` | `fee-items-contract.test.mjs`、`pricing-contract.test.mjs`、`finance-contract.test.mjs` |
|
||||
| 入库测量、首次打印冻结箱码与墨家扫码 | `inbound-measurement.mjs` | `inbound-measurement-box-code-contract.test.mjs` |
|
||||
| 入仓自动头程费用、派生详情、人工发布与 MANUAL 其他费用版本 | `inbound-fees.mjs`(前置:`merchant-qualification-fixture,orders,inbound-measurement`) | `inbound-fees-contract.test.mjs` |
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { test } from 'node:test'
|
||||
|
||||
const entry = readFileSync(new URL('../admin-test-env-acceptance.mjs', import.meta.url), 'utf8')
|
||||
const source = readFileSync(new URL('./inbound-hold.mjs', import.meta.url), 'utf8')
|
||||
|
||||
test('入库扣货真实 HTTP 模块进入默认全量并依赖订单造数', () => {
|
||||
assert.match(entry, /CONFIGURED_MODULES\s*=\s*\[[^\n]*['"]orders['"][^\n]*['"]inbound-hold['"]/)
|
||||
assert.match(entry, /\[['"]inbound-hold['"],\s*\[['"]orders['"]\]\]/)
|
||||
})
|
||||
|
||||
test('入库扣货模块覆盖隔离、幂等、冲突、候选门禁和统一订单投影', () => {
|
||||
assert.match(source, /错误客户不能读取扣货快照/)
|
||||
assert.match(source, /扣货请求同键同载荷重放/)
|
||||
assert.match(source, /扣货同键异载荷返回A0409/)
|
||||
assert.match(source, /扣货入库单从智能装柜候选排除/)
|
||||
assert.match(source, /解除后入库单恢复智能装柜候选/)
|
||||
assert.match(source, /summary\?\.holdFlag/)
|
||||
assert.match(source, /summary\?\.currentHoldReason/)
|
||||
assert.doesNotMatch(source, /\.holdCargo\b|\.holdReason\b|\bholdCargo\s*:|\bholdReason\s*:/)
|
||||
})
|
||||
|
||||
test('入库扣货写操作统一使用请求头 Idempotency-Key 和 expectedVersion', () => {
|
||||
assert.match(source, /context\.request\('POST', path, body, context\.token, \{ 'Idempotency-Key': key \}\)/)
|
||||
assert.match(source, /expectedVersion: initialVersion/)
|
||||
assert.match(source, /expectedVersion: heldVersion/)
|
||||
})
|
||||
@@ -0,0 +1,173 @@
|
||||
const MODULE = 'inbound-hold'
|
||||
const OPTIONS = { scenario: 'inbound-hold-lifecycle', deploymentBlocker: true }
|
||||
const SMART = '/api/platform/v1/first-leg/smart-loading/inbound-candidates'
|
||||
|
||||
function id(value) { return value == null ? '' : String(value) }
|
||||
function code(response) { return id(response?.body?.code) || 'missing' }
|
||||
function succeeded(response) { return response?.http === 200 && code(response) === '200' }
|
||||
function rows(page) { return Array.isArray(page?.records) ? page.records : [] }
|
||||
function channelGroup(channelCode) {
|
||||
const value = String(channelCode || '').toUpperCase()
|
||||
if (value.startsWith('EXX_')) return 'EXX'
|
||||
if (value.startsWith('MATSON_CLX_')) return 'MATSON_CLX'
|
||||
if (value.startsWith('MATSON_MAX_')) return 'MATSON_MAX'
|
||||
if (value.startsWith('HEDE_')) return 'HEDE'
|
||||
if (value.startsWith('OA_LA_') || value === 'OA_PARCEL') return 'OA_LA'
|
||||
if (value.startsWith('OA_CHICAGO_')) return 'OA_CHICAGO'
|
||||
if (value.startsWith('OA_HOUSTON_')) return 'OA_HOUSTON'
|
||||
if (value.startsWith('OA_SAVANNAH_')) return 'OA_SAVANNAH'
|
||||
if (value === 'OA_NEW_YORK_TRUCK') return value
|
||||
throw new Error(`unsupported smart-loading channel group: ${channelCode}`)
|
||||
}
|
||||
function evidence(response, extra = '') {
|
||||
return `HTTP=${response?.http ?? 'missing'}, code=${code(response)}, requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}${extra ? `, ${extra}` : ''}`
|
||||
}
|
||||
function expectCode(context, label, response, expected) {
|
||||
context.assert(MODULE, label, response, response?.http === 200 && code(response) === expected,
|
||||
evidence(response), OPTIONS)
|
||||
}
|
||||
function write(context, path, body, key) {
|
||||
return context.request('POST', path, body, context.token, { 'Idempotency-Key': key })
|
||||
}
|
||||
|
||||
export async function run(context) {
|
||||
const fixture = context.createdResources.orders || {}
|
||||
const tenantId = id(fixture.tenantId)
|
||||
const orderId = id(fixture.orderId)
|
||||
const inboundId = id(fixture.inboundId)
|
||||
const inboundCode = id(fixture.inboundCode)
|
||||
context.assert(MODULE, '复用本轮订单入库单', null,
|
||||
Boolean(tenantId && orderId && inboundId && inboundCode),
|
||||
`tenantId=${tenantId || 'missing'}, orderId=${orderId || 'missing'}, inboundId=${inboundId || 'missing'}, inboundCode=${inboundCode || 'missing'}`,
|
||||
OPTIONS)
|
||||
if (!tenantId || !orderId || !inboundId || !inboundCode) throw new Error('orders fixture missing inbound hold identity')
|
||||
|
||||
const unifiedPath = `/api/platform/v1/orders/${encodeURIComponent(orderId)}?tenantId=${encodeURIComponent(tenantId)}`
|
||||
const initialUnifiedResponse = await context.request('GET', unifiedPath)
|
||||
const initialUnified = context.apiSuccess(MODULE, '扣货前读取统一订单', initialUnifiedResponse,
|
||||
evidence(initialUnifiedResponse), OPTIONS)
|
||||
const mainStatus = initialUnified?.summary?.mainStatus
|
||||
const channelCode = id(initialUnified?.summary?.channelCode)
|
||||
const warehouseId = id(initialUnified?.summary?.receivingWarehouseId || fixture.receivingWarehouseId)
|
||||
context.assert(MODULE, '统一订单提供候选筛选事实', initialUnifiedResponse,
|
||||
Boolean(mainStatus && channelCode && warehouseId),
|
||||
`mainStatus=${mainStatus || 'missing'}, channelCode=${channelCode || 'missing'}, warehouseId=${warehouseId || 'missing'}`,
|
||||
OPTIONS)
|
||||
if (!mainStatus || !channelCode || !warehouseId) throw new Error('unified order missing hold acceptance filters')
|
||||
|
||||
const holdPath = `/api/platform/inbounds/${encodeURIComponent(inboundId)}/hold?tenantId=${encodeURIComponent(tenantId)}`
|
||||
const initialHoldResponse = await context.request('GET', holdPath)
|
||||
const initialHold = context.apiSuccess(MODULE, '读取初始扣货快照', initialHoldResponse,
|
||||
evidence(initialHoldResponse), OPTIONS)
|
||||
context.assert(MODULE, '初始入库单允许扣货', initialHoldResponse,
|
||||
initialHold?.holdFlag === false && initialHold?.canHold === true && initialHold?.canRelease === false
|
||||
&& Array.isArray(initialHold?.history),
|
||||
`holdFlag=${initialHold?.holdFlag}, canHold=${initialHold?.canHold}, canRelease=${initialHold?.canRelease}, history=${initialHold?.history?.length ?? 'missing'}`,
|
||||
OPTIONS)
|
||||
const initialVersion = Number(initialHold?.version)
|
||||
if (!Number.isInteger(initialVersion)) throw new Error('initial inbound hold version missing')
|
||||
|
||||
const candidatePath = `${SMART}?channelGroupCodes=${encodeURIComponent(channelGroup(channelCode))}`
|
||||
+ `&warehouseId=${encodeURIComponent(warehouseId)}&customerId=${encodeURIComponent(tenantId)}`
|
||||
+ `&inboundCode=${encodeURIComponent(inboundCode)}&page=1&size=20`
|
||||
const beforeCandidatesResponse = await context.request('GET', candidatePath)
|
||||
const beforeCandidates = context.apiSuccess(MODULE, '扣货前查询智能装柜候选', beforeCandidatesResponse,
|
||||
evidence(beforeCandidatesResponse), OPTIONS)
|
||||
context.assert(MODULE, '扣货前入库单可进入智能装柜候选', beforeCandidatesResponse,
|
||||
rows(beforeCandidates).some((row) => id(row?.inboundId) === inboundId),
|
||||
`inboundId=${inboundId}, candidates=${rows(beforeCandidates).map((row) => id(row?.inboundId)).join(',') || 'empty'}`,
|
||||
OPTIONS)
|
||||
|
||||
const foreignTenantId = (BigInt(tenantId) + 1n).toString()
|
||||
const foreignResponse = await context.request('GET',
|
||||
`/api/platform/inbounds/${encodeURIComponent(inboundId)}/hold?tenantId=${encodeURIComponent(foreignTenantId)}`)
|
||||
expectCode(context, '错误客户不能读取扣货快照', foreignResponse, 'A0404')
|
||||
|
||||
const reason = `${context.marker} 财务复核扣货`.slice(0, 1000)
|
||||
const holdKey = `${context.marker}-inbound-hold`.slice(0, 128)
|
||||
const holdBody = { expectedVersion: initialVersion, reason }
|
||||
const holdResponse = await write(context, holdPath, holdBody, holdKey)
|
||||
const held = context.apiSuccess(MODULE, '手工扣货入库单', holdResponse, evidence(holdResponse), OPTIONS)
|
||||
context.assert(MODULE, '扣货写入当前快照和不可变历史', holdResponse,
|
||||
held?.holdFlag === true && held?.currentHoldReason === reason && held?.canHold === false
|
||||
&& held?.canRelease === true && Number(held?.version) === initialVersion + 1
|
||||
&& held?.history?.[0]?.action === 'HOLD' && held?.history?.[0]?.reason === reason,
|
||||
`holdFlag=${held?.holdFlag}, version=${held?.version}, history=${held?.history?.length ?? 'missing'}`,
|
||||
OPTIONS)
|
||||
const heldVersion = Number(held?.version)
|
||||
const heldHistorySize = held?.history?.length
|
||||
|
||||
const replayResponse = await write(context, holdPath, holdBody, holdKey)
|
||||
const replayed = context.apiSuccess(MODULE, '扣货请求同键同载荷重放', replayResponse,
|
||||
evidence(replayResponse), OPTIONS)
|
||||
context.assert(MODULE, '扣货幂等重放不增加版本或历史', replayResponse,
|
||||
Number(replayed?.version) === heldVersion && replayed?.history?.length === heldHistorySize,
|
||||
`version=${replayed?.version}, history=${replayed?.history?.length ?? 'missing'}`, OPTIONS)
|
||||
|
||||
const conflictResponse = await write(context, holdPath,
|
||||
{ ...holdBody, reason: `${reason}-冲突` }, holdKey)
|
||||
expectCode(context, '扣货同键异载荷返回A0409', conflictResponse, 'A0409')
|
||||
|
||||
const heldUnifiedResponse = await context.request('GET', unifiedPath)
|
||||
const heldUnified = context.apiSuccess(MODULE, '扣货后读取统一订单', heldUnifiedResponse,
|
||||
evidence(heldUnifiedResponse), OPTIONS)
|
||||
context.assert(MODULE, '统一订单同步展示扣货且主状态不变', heldUnifiedResponse,
|
||||
heldUnified?.summary?.holdFlag === true && heldUnified?.summary?.currentHoldReason === reason
|
||||
&& heldUnified?.summary?.mainStatus === mainStatus,
|
||||
`holdFlag=${heldUnified?.summary?.holdFlag}, mainStatus=${heldUnified?.summary?.mainStatus}`,
|
||||
OPTIONS)
|
||||
|
||||
const heldCandidatesResponse = await context.request('GET', candidatePath)
|
||||
const heldCandidates = context.apiSuccess(MODULE, '扣货后查询智能装柜候选', heldCandidatesResponse,
|
||||
evidence(heldCandidatesResponse), OPTIONS)
|
||||
context.assert(MODULE, '扣货入库单从智能装柜候选排除', heldCandidatesResponse,
|
||||
!rows(heldCandidates).some((row) => id(row?.inboundId) === inboundId),
|
||||
`inboundId=${inboundId}, candidates=${rows(heldCandidates).map((row) => id(row?.inboundId)).join(',') || 'empty'}`,
|
||||
OPTIONS)
|
||||
|
||||
const releasePath = `/api/platform/inbounds/${encodeURIComponent(inboundId)}/hold/release?tenantId=${encodeURIComponent(tenantId)}`
|
||||
const releaseReason = `${context.marker} 财务复核完成`.slice(0, 1000)
|
||||
const releaseKey = `${context.marker}-inbound-release`.slice(0, 128)
|
||||
const releaseBody = { expectedVersion: heldVersion, reason: releaseReason }
|
||||
const releaseResponse = await write(context, releasePath, releaseBody, releaseKey)
|
||||
const released = context.apiSuccess(MODULE, '解除入库单扣货', releaseResponse,
|
||||
evidence(releaseResponse), OPTIONS)
|
||||
context.assert(MODULE, '解除写入当前快照和不可变历史', releaseResponse,
|
||||
released?.holdFlag === false && released?.currentHoldReason == null && released?.canHold === true
|
||||
&& released?.canRelease === false && Number(released?.version) === heldVersion + 1
|
||||
&& released?.history?.[0]?.action === 'RELEASE' && released?.history?.[0]?.reason === releaseReason
|
||||
&& released?.history?.some?.((item) => item.action === 'HOLD' && item.reason === reason),
|
||||
`holdFlag=${released?.holdFlag}, version=${released?.version}, history=${released?.history?.length ?? 'missing'}`,
|
||||
OPTIONS)
|
||||
const releasedVersion = Number(released?.version)
|
||||
const releasedHistorySize = released?.history?.length
|
||||
|
||||
const releaseReplayResponse = await write(context, releasePath, releaseBody, releaseKey)
|
||||
const releaseReplay = context.apiSuccess(MODULE, '解除请求同键同载荷重放', releaseReplayResponse,
|
||||
evidence(releaseReplayResponse), OPTIONS)
|
||||
context.assert(MODULE, '解除幂等重放不增加版本或历史', releaseReplayResponse,
|
||||
Number(releaseReplay?.version) === releasedVersion && releaseReplay?.history?.length === releasedHistorySize,
|
||||
`version=${releaseReplay?.version}, history=${releaseReplay?.history?.length ?? 'missing'}`, OPTIONS)
|
||||
|
||||
const staleResponse = await write(context, holdPath,
|
||||
{ expectedVersion: heldVersion, reason: `${context.marker} 过期版本` },
|
||||
`${context.marker}-inbound-hold-stale`.slice(0, 128))
|
||||
expectCode(context, '过期版本再次扣货返回A0409', staleResponse, 'A0409')
|
||||
|
||||
const finalUnifiedResponse = await context.request('GET', unifiedPath)
|
||||
const finalUnified = context.apiSuccess(MODULE, '解除后读取统一订单', finalUnifiedResponse,
|
||||
evidence(finalUnifiedResponse), OPTIONS)
|
||||
context.assert(MODULE, '解除后统一订单恢复且主状态不变', finalUnifiedResponse,
|
||||
finalUnified?.summary?.holdFlag === false && finalUnified?.summary?.currentHoldReason == null
|
||||
&& finalUnified?.summary?.mainStatus === mainStatus,
|
||||
`holdFlag=${finalUnified?.summary?.holdFlag}, mainStatus=${finalUnified?.summary?.mainStatus}`,
|
||||
OPTIONS)
|
||||
|
||||
const finalCandidatesResponse = await context.request('GET', candidatePath)
|
||||
const finalCandidates = context.apiSuccess(MODULE, '解除后查询智能装柜候选', finalCandidatesResponse,
|
||||
evidence(finalCandidatesResponse), OPTIONS)
|
||||
context.assert(MODULE, '解除后入库单恢复智能装柜候选', finalCandidatesResponse,
|
||||
rows(finalCandidates).some((row) => id(row?.inboundId) === inboundId),
|
||||
`inboundId=${inboundId}, candidates=${rows(finalCandidates).map((row) => id(row?.inboundId)).join(',') || 'empty'}`,
|
||||
OPTIONS)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const unified = readFileSync(new URL('../deploy/sql/头程数据库统一增量-MySQL8.sql', import.meta.url), 'utf8')
|
||||
const firstleg = readFileSync(new URL('../firstleg/src/main/resources/db/phase2-inbound-schema.sql', import.meta.url), 'utf8')
|
||||
const iam = readFileSync(new URL('../iam/src/main/resources/db/inbound-hold-permission.sql', import.meta.url), 'utf8')
|
||||
|
||||
test('统一增量与 Firstleg 扣货快照和不可变历史结构一致', () => {
|
||||
for (const source of [unified, firstleg]) {
|
||||
assert.match(source, /current_hold_reason VARCHAR\(1000\) NULL COMMENT '当前扣货理由快照'/)
|
||||
assert.match(source, /CREATE TABLE IF NOT EXISTS fl_inbound_hold_record/)
|
||||
assert.match(source, /hold_action VARCHAR\(16\) NOT NULL COMMENT '动作:HOLD或RELEASE'/)
|
||||
assert.match(source, /reason VARCHAR\(1000\) NOT NULL/)
|
||||
assert.match(source, /evidence_file_id BIGINT UNSIGNED NULL/)
|
||||
assert.match(source, /KEY idx_hold_inbound_time \(tenant_id, inbound_id, operated_at\)/)
|
||||
}
|
||||
assert.match(unified, /MODIFY COLUMN current_hold_reason VARCHAR\(1000\) NULL/)
|
||||
})
|
||||
|
||||
test('统一增量与 IAM 模块冻结同一独立扣货权限且只默认授予平台管理员', () => {
|
||||
for (const source of [unified, iam]) {
|
||||
assert.match(source, /\(5102032, 0, 'PLATFORM', 'BUTTON', 5102030, 'platform:firstleg:inbound:hold'/)
|
||||
assert.match(source, /role\.role_code='PLATFORM_ADMIN'/)
|
||||
assert.match(source, /permission\.permission_code='platform:firstleg:inbound:hold'/)
|
||||
assert.match(source, /入库单扣货权限初始化失败:权限ID、编码或类型冲突/)
|
||||
assert.match(source, /入库单扣货权限初始化失败:权限资源不完整/)
|
||||
assert.match(source, /入库单扣货权限初始化失败:平台超级管理员授权不完整/)
|
||||
}
|
||||
const migration = unified.slice(unified.indexOf('CREATE PROCEDURE migrate_inbound_hold_permission()'),
|
||||
unified.indexOf('DROP PROCEDURE IF EXISTS assert_wallet_event_schema'))
|
||||
assert.doesNotMatch(migration, /role_code='(?:ORDER_OPERATOR|WAREHOUSE_OPERATOR|WAREHOUSE_TALLY|PLATFORM_CUSTOM_ADMIN)'/)
|
||||
})
|
||||
Reference in New Issue
Block a user