fix: isolate mobile presign action scopes and transfer locks.
This commit is contained in:
1 parent
7a750e7e81
commit
a3a2ffcce0
4 files changed
+124
-33
No files matched your search
@@ -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') {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in new issue
Block a user