fix: isolate mobile presign action scopes and transfer locks.

This commit is contained in:
zlwu committed 2026-10-11 12:59:22 +08:00
1 parent 7a750e7e81
commit a3a2ffcce0
4 files changed
+124 -33

No files matched your search

+53 -19
View File
@@ -26,6 +26,29 @@ const ACTION_ALIASES = Object.freeze({
'withdraw-transfer': 'withdraw'
})
const PRESIGN_TOP_ACTIONS = new Set([
'view',
'create',
'edit',
'convert',
'delete',
'create-transfer'
])
const PRESIGN_ORDER_ACTIONS = new Set([
'edit',
'convert',
'delete',
'create-transfer'
])
const PRESIGN_TRANSFER_ACTION_MAP = Object.freeze({
edit: 'edit-presign-transfer',
delete: 'delete-transfer',
submit: 'submit-transfer',
withdraw: 'withdraw-transfer'
})
export function moduleActions(module, role) {
return [...(ACTIONS[module] || [])]
}
@@ -45,7 +68,9 @@ function normalizeAction(action) {
}
export function intersectAllowedActions(module, role, backendActions) {
const whitelist = new Set(moduleActions(module, role))
const whitelist = module === 'presign'
? PRESIGN_TOP_ACTIONS
: new Set(moduleActions(module, role))
return (Array.isArray(backendActions) ? backendActions : [])
.map((action) => {
const original = typeof action === 'string'
@@ -62,36 +87,45 @@ export function intersectAllowedActions(module, role, backendActions) {
export function collectAllowedActions(module, role, detail) {
if (module === 'presign') {
const whitelist = new Set(moduleActions(module, role))
const transferActionMap = {
edit: 'edit-presign-transfer',
delete: 'delete-transfer',
submit: 'submit-transfer',
withdraw: 'withdraw-transfer'
}
const frontendWhitelist = new Set(moduleActions(module, role))
const nested = []
for (const transfer of detail?.transfers || []) {
for (const action of transfer?.allowedActions || []) {
const normalized = normalizeAction(action)
if (!normalized) continue
const backendCode = typeof normalized === 'string' ? normalized : normalized.code
const code = transferActionMap[backendCode] || backendCode
if (!whitelist.has(code)) continue
const backendCode = typeof action === 'string'
? action
: String(action?.code || action?.action || action?.key || '').trim()
const code = PRESIGN_TRANSFER_ACTION_MAP[backendCode]
if (!code || !frontendWhitelist.has(code)) continue
const metadata = typeof action === 'string'
? {}
: (() => {
const { key: _key, action: _action, ...rest } = action
return rest
})()
nested.push({
...(typeof normalized === 'string' ? {} : normalized),
...metadata,
code,
transferId: transfer.id,
actionKey: `${code}:transfer:${transfer.id}`
})
}
}
const top = (Array.isArray(detail?.allowedActions || detail?.actions)
? detail.allowedActions || detail.actions
: [])
.map((action) => {
const code = typeof action === 'string'
? action
: String(action?.code || action?.action || action?.key || '').trim()
if (!PRESIGN_ORDER_ACTIONS.has(code)) return null
if (typeof action === 'string') return action
const { key: _key, action: _action, ...metadata } = action
return { ...metadata, code }
})
.filter(Boolean)
return [
...nested,
...intersectAllowedActions(
module,
role,
detail?.allowedActions || detail?.actions
)
...top
]
}
if (module !== 'payment') {
+48 -2
View File
@@ -128,7 +128,7 @@ test('presign actions never exceed the backend authorization set', () => {
'finance-confirm',
'admin-only'
]),
['delete', 'create-transfer', 'withdraw-transfer']
['delete', 'create-transfer']
)
assert.equal(
intersectAllowedActions('presign', 'specialist', ['edit']).includes('delete'),
@@ -136,9 +136,55 @@ test('presign actions never exceed the backend authorization set', () => {
)
})
test('presign order actions reject transfer-only backend action codes', () => {
assert.deepEqual(
intersectAllowedActions('presign', 'specialist', [
'create',
'edit',
'convert',
'delete',
'create-transfer',
'edit-presign-transfer',
'delete-transfer',
'submit-transfer',
'withdraw-transfer'
]),
['create', 'edit', 'convert', 'delete', 'create-transfer']
)
})
test('presign transfer actions accept only raw transfer authorization codes without mutation', () => {
const detail = {
allowedActions: [],
transfers: [{
id: 'transfer-1',
allowedActions: [
'edit',
'delete',
'submit',
'withdraw',
'convert',
'create-transfer',
'edit-presign-transfer'
]
}]
}
const before = structuredClone(detail)
const actions = collectAllowedActions('presign', 'manager', detail)
assert.deepEqual(actions.map((item) => item.code), [
'edit-presign-transfer',
'delete-transfer',
'submit-transfer',
'withdraw-transfer'
])
assert.deepEqual(detail, before)
})
test('collects only backend-authorized presign order and nested transfer actions', () => {
const actions = collectAllowedActions('presign', 'manager', {
allowedActions: ['edit', 'convert', 'delete', 'admin-only'],
allowedActions: ['create', 'edit', 'convert', 'delete', 'withdraw-transfer', 'admin-only'],
transfers: [
{ id: 'draft', allowedActions: ['edit', 'delete', 'submit', 'finance-confirm'] },
{ id: 'pending', allowedActions: ['withdraw'] },
@@ -81,10 +81,14 @@ export function isPresignLocked(form) {
return String(form?.orderStatus || '').trim() === 'converted'
}
export function isPresignTransferLocked(transfer, order) {
if (isPresignLocked(order || transfer)) return true
const status = String(transfer?.payStatus || '').trim()
return Boolean(status && status !== 'dfj')
export function isPresignTransferLocked(transfer, orderOrStatus) {
const orderStatus = String(
typeof orderOrStatus === 'string'
? orderOrStatus
: orderOrStatus?.orderStatus ?? transfer?.orderStatus ?? ''
).trim()
if (!['', 'draft', 'active'].includes(orderStatus)) return true
return String(transfer?.payStatus || '').trim() !== 'dfj'
}
function attachmentUrls(transfer) {
@@ -66,17 +66,24 @@ test('changing region clears stale hospital and manager selections', () => {
)
})
test('locks converted orders and every transfer status except draft payment', () => {
test('keeps void orders editable while converted orders are locked', () => {
assert.equal(isPresignLocked({ orderStatus: 'converted' }), true)
assert.equal(isPresignLocked({ orderStatus: 'active' }), false)
assert.equal(isPresignLocked({ orderStatus: 'void' }), false)
})
test('allows draft-payment transfers only under writable parent order states', () => {
assert.equal(isPresignTransferLocked({ payStatus: 'dfj' }), false)
assert.equal(isPresignTransferLocked({ payStatus: '' }), false)
assert.equal(isPresignTransferLocked({ payStatus: 'jfqrz' }), true)
assert.equal(isPresignTransferLocked({ payStatus: 'yjf' }), true)
assert.equal(
isPresignTransferLocked({ payStatus: 'dfj' }, { orderStatus: 'converted' }),
true
)
assert.equal(isPresignTransferLocked({ payStatus: 'dfj' }, { orderStatus: 'draft' }), false)
assert.equal(isPresignTransferLocked({ payStatus: 'dfj' }, { orderStatus: 'active' }), false)
assert.equal(isPresignTransferLocked({ payStatus: 'dfj' }, 'active'), false)
assert.equal(isPresignTransferLocked({ payStatus: 'dfj' }, { orderStatus: 'void' }), true)
assert.equal(isPresignTransferLocked({ payStatus: 'dfj' }, { orderStatus: 'converted' }), true)
assert.equal(isPresignTransferLocked({ payStatus: 'dfj' }, { orderStatus: 'unknown' }), true)
assert.equal(isPresignTransferLocked({ payStatus: '' }, { orderStatus: 'active' }), true)
assert.equal(isPresignTransferLocked({ payStatus: 'jfqrz' }, { orderStatus: 'active' }), true)
assert.equal(isPresignTransferLocked({ payStatus: 'yjf' }, { orderStatus: 'active' }), true)
})
test('builds an order payload from only backend-writable camelCase fields', () => {