356 lines
25 KiB
TypeScript
356 lines
25 KiB
TypeScript
import { readFileSync } from 'node:fs'
|
|
import { resolve } from 'node:path'
|
|
import ElementPlus, { ElMessage } from 'element-plus'
|
|
import { createPinia, setActivePinia } from 'pinia'
|
|
import { flushPromises, mount } from '@vue/test-utils'
|
|
import { createMemoryHistory, createRouter } from 'vue-router'
|
|
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
|
import { useAuthStore } from '@/core/auth/auth'
|
|
import { createPlatformExportTask } from '@/modules/downloads'
|
|
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
|
import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog'
|
|
import { adjustFinanceAccount, downloadFinanceRechargeVoucher, listFinanceAccountLedger, listFinanceAccounts, listFinanceRechargeRequests, reviewFinanceRechargeRequest, updateFinanceCreditLimit } from '../api/finance'
|
|
import FinanceCustomerAccountsView from './FinanceCustomerAccountsView.vue'
|
|
|
|
vi.mock('../api/finance', () => ({
|
|
listFinanceAccounts: vi.fn(),
|
|
listFinanceAccountLedger: vi.fn(),
|
|
listFinanceRechargeRequests: vi.fn(),
|
|
reviewFinanceRechargeRequest: vi.fn(),
|
|
adjustFinanceAccount: vi.fn(),
|
|
updateFinanceCreditLimit: vi.fn(),
|
|
downloadFinanceRechargeVoucher: vi.fn(),
|
|
getFinanceCustomerTerms: vi.fn(),
|
|
updateFinanceCustomerTerms: vi.fn(),
|
|
}))
|
|
vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn() }))
|
|
vi.mock('@/modules/downloads', () => ({ createPlatformExportTask: vi.fn() }))
|
|
vi.mock('@/shared/components/exportTaskCreatedDialog', () => ({ showExportTaskCreatedDialog: vi.fn() }))
|
|
|
|
class ResizeObserverStub implements ResizeObserver { observe() {} unobserve() {} disconnect() {} }
|
|
|
|
function memoryStorage(): Storage {
|
|
const values = new Map<string, string>()
|
|
return { get length() { return values.size }, clear: () => values.clear(), getItem: (key) => values.get(key) ?? null, key: (index) => [...values.keys()][index] ?? null, removeItem: (key) => values.delete(key), setItem: (key, value) => values.set(key, value) }
|
|
}
|
|
|
|
const accountFixture = {
|
|
customerId: '10001', customerCode: 'C-001', customerName: '客户A', contactName: '张三', contactPhone: '13800000000',
|
|
outstandingReceivable: 100, pendingBilling: 20, receivedInPeriod: 30, totalCost: 90, paidCost: 50, outstandingPayable: 40,
|
|
actualBalance: '126580.50', frozenBalance: '8500.00', availableBalance: '138080.50', creditLimit: '20000.00', creditUsed: '0.00', availableCredit: '20000.00', creditExpiresOn: '2028-09-10', creditActive: true, accountVersion: 7, pendingRechargeCount: 1,
|
|
status: 'ACTIVE', lastChangedAt: '2026-08-13 10:00:00',
|
|
}
|
|
|
|
async function mountAccountsView(permissions: string[], initialPath = '/finance/customer-accounts') {
|
|
const pinia = createPinia(); setActivePinia(pinia)
|
|
const auth = useAuthStore(); auth.token = 'token'; auth.permissions = permissions
|
|
useAccessPreviewStore().initializeAfterLogin()
|
|
const router = createRouter({ history: createMemoryHistory(), routes: [
|
|
{ path: '/finance/customer-accounts', name: 'finance-customer-accounts', component: FinanceCustomerAccountsView },
|
|
{ path: '/finance/customer-accounts/:customerId', name: 'finance-customer-account-detail', component: FinanceCustomerAccountsView },
|
|
{ path: '/downloads', name: 'download-center', component: { template: '<div>下载中心</div>' } },
|
|
] })
|
|
await router.push(initialPath); await router.isReady()
|
|
const wrapper = mount(FinanceCustomerAccountsView, { global: { plugins: [pinia, ElementPlus, router] } })
|
|
await flushPromises()
|
|
return Object.assign(wrapper, { router })
|
|
}
|
|
|
|
describe('FinanceCustomerAccountsView delivery', () => {
|
|
const page = readFileSync(resolve(process.cwd(), 'src/modules/finance/pages/FinanceCustomerAccountsView.vue'), 'utf8')
|
|
const routes = readFileSync(resolve(process.cwd(), 'src/modules/finance/routes.ts'), 'utf8')
|
|
|
|
it('keeps the account detail as a full page with traceable ledger controls', () => {
|
|
expect(page).toContain('v-else-if="!detailOpen"')
|
|
expect(page).toContain('返回客户账户')
|
|
expect(page).toContain('<DetailPageLayout')
|
|
expect(page).toContain('class="density-detail-overview account-metrics"')
|
|
for (const metric of ['实际余额', '冻结金额', '总可用余额', '授信额度', '已用授信', '可用授信']) expect(page).toContain(metric)
|
|
expect(page).toContain('class="ledger-query"')
|
|
expect(page).toContain('@search="ledgerPage = 1; void loadLedger($event)"')
|
|
expect(page).toContain('class="ledger-table"')
|
|
expect(page).toContain('<template #toolbar-actions>')
|
|
expect(page).toContain('<template #pagination-left>共 {{ ledgerTotal }} 条</template>')
|
|
expect(page).not.toContain('class="detail-toolbar"')
|
|
expect(page).not.toContain('<AppDrawer')
|
|
for (const marker of ['充值申请', '待审核', '审核', '流水类型', '发生日期', '导出流水', 'listFinanceAccountLedger', 'listFinanceRechargeRequests']) expect(page).toContain(marker)
|
|
for (const code of ['RECONCILIATION_CREATED', 'SETTLEMENT_ISSUED', 'RECEIPT',
|
|
'SUPPLIER_DIRECT_PAID', 'SUPPLIER_ON_ACCOUNT_PAYMENT', 'ADJUSTMENT', 'VOID']) expect(page).toContain(code)
|
|
for (const removed of ['FEE_INCLUDED', 'FEE_EXCLUDED', '纳入结算', '排除结算']) expect(page).not.toContain(removed)
|
|
for (const obsolete of ["value: 'FEE_SOURCE'", "value: 'RECONCILIATION'", "value: 'SETTLEMENT'"]) expect(page).not.toContain(obsolete)
|
|
})
|
|
|
|
it('keeps term editing aligned with the account summary and nearby actions', () => {
|
|
for (const marker of ['class="term-section density-form-section"', 'class="term-editor"', '账期天数', '#footer', '保存账期']) expect(page).toContain(marker)
|
|
expect(page).toContain('density-detail-overview account-metrics')
|
|
for (const column of ['实际余额(CNY)', '冻结金额(CNY)', '总可用余额(CNY)', '授信额度(CNY)', '已用授信(CNY)', '待审核充值']) expect(page).toContain(column)
|
|
expect(page).toContain('grid-template-columns: 120px minmax(0, 220px)')
|
|
expect(page).toContain('justify-content: start')
|
|
expect(page).toContain('class="surface-panel finance-account-detail__surface"')
|
|
expect(page).toContain('class="density-detail-header"')
|
|
expect(page).toContain('padding: var(--admin-space-4) 0')
|
|
expect(page).toContain('class="detail-table-scroll"')
|
|
expect(page).toContain('class="ledger-identifier"')
|
|
expect(page).toContain('class="ledger-document"')
|
|
expect(page).toContain('text-overflow: ellipsis')
|
|
expect(page).toContain('white-space: nowrap')
|
|
})
|
|
|
|
it('keeps account detail and term editing readable across the 1920 workspace', () => {
|
|
expect(page).toContain('width: min(100%, 1920px)')
|
|
expect(page).toContain('@media (min-width: 1920px)')
|
|
expect(page).toContain('finance-density-detail')
|
|
expect(page).toContain('../financeDensity.css')
|
|
expect(page).not.toContain('max-width: 520px')
|
|
})
|
|
|
|
it('keeps high-frequency account actions visible and groups the remaining actions', () => {
|
|
expect(page).toContain("{ key: 'actions', label: '操作', width: 290, fixed: 'left', slotName: 'actions' }")
|
|
expect(page).toContain("import RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue'")
|
|
expect(page).toContain("{ key: 'detail', label: '详情', priority: 300 }")
|
|
expect(page).toContain("{ key: 'recharge', label: '充值', priority: 250")
|
|
expect(page).toContain("{ key: 'deduction', label: '扣款', priority: 200")
|
|
expect(page).toContain("{ key: 'credit', label: '授信', priority: 100")
|
|
expect(page).toContain("{ key: 'terms', label: '账期', visible: canUpdateTerms.value")
|
|
expect(page).toContain('<RowActions :actions="accountActions(row as FinanceAccountSummary)"')
|
|
})
|
|
|
|
it('shows authoritative CNY wallet balances and independently permitted actions', () => {
|
|
for (const marker of ['实际余额', '冻结金额', '总可用余额', '授信额度', '已用授信', '可用授信', '手工充值', '手工扣款', '设置授信', 'platform:finance:customer-account:adjust', 'platform:finance:recharge:review']) expect(page).toContain(marker)
|
|
expect(page).not.toContain('operatingFundsSummary')
|
|
expect(page).not.toContain('adjustmentAccount?.actualBalance -')
|
|
})
|
|
|
|
it.each([
|
|
[[], ['详情'], false],
|
|
[['platform:finance:customer-terms:view', 'platform:finance:customer-terms:update'], ['详情', '账期'], false],
|
|
[['platform:finance:customer-account:adjust'], ['详情', '充值', '扣款', ''], true],
|
|
])('folds account operations only when more than three are available: %j', async (extraPermissions, labels, overflow) => {
|
|
const wrapper = await mountAccountsView(['platform:finance:customer-account:list', ...extraPermissions])
|
|
const actions = wrapper.get('.el-table__body-wrapper .density-actions')
|
|
expect(actions.findAll('button').map(button => button.text().trim())).toEqual(labels)
|
|
expect(actions.find('.el-dropdown').exists()).toBe(overflow)
|
|
if (overflow) {
|
|
const trigger = actions.get('button[aria-label^="更多客户账户操作"]')
|
|
expect(trigger.text()).toBe('')
|
|
expect(trigger.find('svg').exists()).toBe(true)
|
|
}
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('selects an active customer by name from master data', () => {
|
|
expect(page).toContain("key: 'customerId', type: 'select'")
|
|
expect(page).toContain("label: '客户名称'")
|
|
expect(page).toContain('item.tenantName')
|
|
expect(page).not.toContain('tenantCode')
|
|
expect(page).toContain('listPlatformTenants')
|
|
expect(page).not.toContain("key: 'keyword', type: 'input'")
|
|
})
|
|
|
|
it('supports a refreshable customer detail route without inventing an API parameter', () => {
|
|
expect(routes).toContain("path: 'finance/customer-accounts/:customerId'")
|
|
expect(page).toContain("name: 'finance-customer-account-detail'")
|
|
expect(page).toContain('restoreAccountDetail(customerId)')
|
|
expect(page).toContain("listFinanceAccounts({ page: 1, size: 1, customerId })")
|
|
expect(page).toContain('customerId: String(filters.value.customerId ||')
|
|
})
|
|
|
|
beforeEach(() => {
|
|
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
|
|
vi.stubGlobal('localStorage', memoryStorage())
|
|
vi.stubGlobal('sessionStorage', memoryStorage())
|
|
vi.stubGlobal('URL', { createObjectURL: vi.fn(() => 'blob:finance-voucher'), revokeObjectURL: vi.fn() })
|
|
vi.mocked(listFinanceAccounts).mockReset().mockResolvedValue({ records: [accountFixture], total: 1, pages: 1 })
|
|
vi.mocked(listFinanceAccountLedger).mockReset().mockResolvedValue({ records: [{ ledgerId: '1', occurredAt: '2026-08-13T10:00:00', businessType: 'REC', documentType: 'RECEIPT', documentId: '2', documentNo: 'REC-2', receivableChange: -10, payableChange: 0, outstandingReceivableAfter: 90, operatorName: 'operator-1000001', remark: '收款', businessNo: 'AC-1', changeType: 'RECHARGE', amount: '500.00', actualBalanceAfter: '126580.50', frozenBalanceAfter: '8500.00', availableBalanceAfter: '118080.50', sourceType: 'MANUAL_ADJUSTMENT', sourceNo: 'RC-2', createdAt: '2026-08-13T10:00:00' }], total: 1, pages: 1 })
|
|
vi.mocked(listFinanceRechargeRequests).mockReset().mockResolvedValue({ records: [{ requestId: '91', requestNo: 'RC-91', amount: '500.00', voucherFileId: '81', voucherFileName: 'receipt.png', status: 'PENDING', version: 3, submittedByName: '客户A', reviewedByName: '', reviewRemark: '', createdAt: '2026-09-04T10:00:00', reviewedAt: '' }], total: 1, pages: 1 })
|
|
vi.mocked(reviewFinanceRechargeRequest).mockReset().mockResolvedValue({ requestId: '91', requestNo: 'RC-91', amount: '500.00', voucherFileId: '81', voucherFileName: 'receipt.png', status: 'APPROVED', version: 4, submittedByName: '客户A', reviewedByName: '财务A', reviewRemark: '', createdAt: '2026-09-04T10:00:00', reviewedAt: '2026-09-05T10:00:00' })
|
|
vi.mocked(adjustFinanceAccount).mockReset().mockResolvedValue({ ...accountFixture, actualBalance: '127080.50', availableBalance: '118580.50', accountVersion: 8 })
|
|
vi.mocked(updateFinanceCreditLimit).mockReset().mockResolvedValue({ ledgerId: '102', businessNo: 'AC-102', changeType: 'CREDIT_LIMIT', amount: '20000.00', actualBalanceAfter: '126580.50', frozenBalanceAfter: '8500.00', availableBalanceAfter: '138080.50', creditLimitAfter: '20000.00', creditUsedAfter: '0.00', availableCreditAfter: '20000.00', creditExpiresOnAfter: '2028-09-10', sourceType: 'CREDIT_LIMIT_ADJUSTMENT', sourceNo: '', occurredAt: '', occurredDate: '2026-09-05', paymentMethod: '', referenceNo: '', operatorName: '财务A', remark: '', createdAt: '2026-09-05T10:00:00' })
|
|
vi.mocked(downloadFinanceRechargeVoucher).mockReset().mockResolvedValue(new Blob(['image'], { type: 'image/png' }))
|
|
vi.mocked(listPlatformTenants).mockReset().mockResolvedValue({ records: [{ id: '10001', tenantName: '客户A', status: 'ACTIVE' } as PlatformTenantResource], page: 1, size: 200, total: 1, pages: 1 })
|
|
vi.mocked(createPlatformExportTask).mockReset().mockResolvedValue({ id: 'exp-1', taskNo: 'EXP-1', taskName: '客户账户', status: 'PENDING' } as never)
|
|
vi.mocked(showExportTaskCreatedDialog).mockReset().mockResolvedValue(false)
|
|
})
|
|
|
|
it('restores account summary and ledger after opening the detail URL directly', async () => {
|
|
const pinia = createPinia(); setActivePinia(pinia)
|
|
const auth = useAuthStore(); auth.token = 'token'; auth.permissions = ['platform:finance:customer-account:list']
|
|
useAccessPreviewStore().initializeAfterLogin()
|
|
const router = createRouter({ history: createMemoryHistory(), routes: [
|
|
{ path: '/finance/customer-accounts', name: 'finance-customer-accounts', component: FinanceCustomerAccountsView },
|
|
{ path: '/finance/customer-accounts/:customerId', name: 'finance-customer-account-detail', component: FinanceCustomerAccountsView },
|
|
] })
|
|
await router.push('/finance/customer-accounts/10001'); await router.isReady()
|
|
const wrapper = mount(FinanceCustomerAccountsView, { global: { plugins: [pinia, ElementPlus, router] } })
|
|
await flushPromises()
|
|
|
|
expect(wrapper.text()).toContain('客户A')
|
|
expect(wrapper.text()).toContain('C-001')
|
|
expect(wrapper.text()).toContain('张三')
|
|
expect(wrapper.text()).toContain('13800000000')
|
|
const metrics = wrapper.find('.account-metrics')
|
|
expect(metrics.findAll('.account-metric')).toHaveLength(6)
|
|
expect(metrics.text()).toContain('实际余额¥126580.50 CNY')
|
|
expect(metrics.text()).toContain('冻结金额¥8500.00 CNY')
|
|
expect(metrics.text()).toContain('总可用余额¥138080.50 CNY')
|
|
expect(metrics.text()).toContain('授信额度¥20000.00 2028-09-10 到期')
|
|
expect(metrics.text()).toContain('已用授信¥0.00 CNY')
|
|
expect(metrics.text()).toContain('可用授信¥20000.00 CNY')
|
|
for (const auxiliary of ['待开账金额', '本期已收', '总成本', '已付成本', '已收客户款', '客户线下支付']) expect(metrics.text()).not.toContain(auxiliary)
|
|
expect(listFinanceAccounts).toHaveBeenCalledWith({ page: 1, size: 1, customerId: '10001' })
|
|
expect(listFinanceAccountLedger).toHaveBeenCalledWith('10001', expect.objectContaining({ page: 1, size: 20 }))
|
|
expect(listFinanceRechargeRequests).toHaveBeenCalledWith('10001', { page: 1, size: 200 })
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('shows explicit master-data empty states without exposing the customer id', async () => {
|
|
vi.mocked(listFinanceAccounts).mockResolvedValue({ records: [{ ...accountFixture, customerCode: '', contactName: '', contactPhone: '', actualBalance: '0.00', frozenBalance: '0.00', availableBalance: '0.00', pendingRechargeCount: 0, lastChangedAt: '' }], total: 1, pages: 1 })
|
|
const wrapper = await mountAccountsView(['platform:finance:customer-account:list'])
|
|
expect(wrapper.text()).toContain('客户编号')
|
|
expect(wrapper.text()).toContain('联系人')
|
|
expect(wrapper.text()).toContain('联系电话')
|
|
expect(wrapper.text().match(/未维护/g)?.length).toBeGreaterThanOrEqual(3)
|
|
expect(wrapper.text()).not.toContain('客户10001')
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('formats known ledger codes, operator ids and timestamps for display', async () => {
|
|
vi.mocked(listFinanceAccountLedger).mockResolvedValue({ records: [{ ledgerId: '2087867025978863617', occurredAt: '2026-08-13T10:00:00', businessType: 'SETTLEMENT_ISSUED', documentType: 'BILL', documentId: '2', documentNo: 'REC-00000000000000000001', receivableChange: 10, payableChange: 0, outstandingReceivableAfter: 90, operatorName: 'operator-1000001', remark: '结算单生效' }], total: 1, pages: 1 })
|
|
const pinia = createPinia(); setActivePinia(pinia)
|
|
const auth = useAuthStore(); auth.token = 'token'; auth.permissions = ['platform:finance:customer-account:list']
|
|
useAccessPreviewStore().initializeAfterLogin()
|
|
const router = createRouter({ history: createMemoryHistory(), routes: [
|
|
{ path: '/finance/customer-accounts', name: 'finance-customer-accounts', component: FinanceCustomerAccountsView },
|
|
{ path: '/finance/customer-accounts/:customerId', name: 'finance-customer-account-detail', component: FinanceCustomerAccountsView },
|
|
] })
|
|
await router.push('/finance/customer-accounts/10001'); await router.isReady()
|
|
const wrapper = mount(FinanceCustomerAccountsView, { global: { plugins: [pinia, ElementPlus, router] } })
|
|
await flushPromises()
|
|
expect(wrapper.text()).toContain('结算单生效')
|
|
expect(wrapper.text()).toContain('REC-00000000000000000001')
|
|
expect(wrapper.text()).toContain('名称数据缺失')
|
|
expect(wrapper.text()).not.toContain('operator-1000001')
|
|
expect(wrapper.text()).not.toContain('1000001')
|
|
expect(wrapper.text()).not.toContain('2026-08-13T10:00:00')
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('shows recharge and deduction only with the independent adjustment permission', async () => {
|
|
const readOnly = await mountAccountsView(['platform:finance:customer-account:list'])
|
|
expect(readOnly.findAll('button').map(button => button.text())).not.toEqual(expect.arrayContaining(['充值', '扣款']))
|
|
readOnly.unmount()
|
|
|
|
const writable = await mountAccountsView(['platform:finance:customer-account:list', 'platform:finance:customer-account:adjust'])
|
|
expect(writable.findAll('button').map(button => button.text())).toEqual(expect.arrayContaining(['充值', '扣款']))
|
|
writable.unmount()
|
|
})
|
|
|
|
it('submits a two-decimal adjustment with account version and refreshes detail data', async () => {
|
|
const wrapper = await mountAccountsView(['platform:finance:customer-account:list', 'platform:finance:customer-account:adjust'], '/finance/customer-accounts/10001')
|
|
const vm = wrapper.vm as unknown as {
|
|
adjustmentForm: Record<string, string>
|
|
openAdjustment(account: typeof accountFixture, type: 'RECHARGE' | 'DEDUCTION'): void
|
|
submitAdjustment(): Promise<void>
|
|
}
|
|
vm.openAdjustment(accountFixture, 'DEDUCTION')
|
|
Object.assign(vm.adjustmentForm, { amount: '100', occurredDate: '2026-09-05', paymentMethod: 'BANK_TRANSFER', referenceNo: 'OUT-1', remark: ' 贴牌费 ' })
|
|
await vm.submitAdjustment()
|
|
|
|
expect(adjustFinanceAccount).toHaveBeenCalledWith('10001', {
|
|
type: 'DEDUCTION', amount: '100.00', occurredDate: '2026-09-05', paymentMethod: 'BANK_TRANSFER',
|
|
referenceNo: 'OUT-1', remark: '贴牌费', expectedAccountVersion: 7,
|
|
})
|
|
expect(listFinanceAccounts).toHaveBeenLastCalledWith({ page: 1, size: 1, customerId: '10001' })
|
|
expect(listFinanceRechargeRequests).toHaveBeenCalledTimes(2)
|
|
expect(listFinanceAccountLedger).toHaveBeenCalledTimes(2)
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('keeps pending recharge requests first and reviews with request version', async () => {
|
|
vi.mocked(listFinanceRechargeRequests).mockResolvedValue({ records: [
|
|
{ requestId: '92', requestNo: 'RC-92', amount: '20.00', voucherFileId: '82', voucherFileName: 'paid.png', status: 'APPROVED', version: 4, submittedByName: '客户A', reviewedByName: '财务A', reviewRemark: '', createdAt: '2026-09-03T10:00:00', reviewedAt: '2026-09-04T10:00:00' },
|
|
{ requestId: '91', requestNo: 'RC-91', amount: '500.00', voucherFileId: '81', voucherFileName: 'receipt.png', status: 'PENDING', version: 3, submittedByName: '客户A', reviewedByName: '', reviewRemark: '', createdAt: '2026-09-04T10:00:00', reviewedAt: '' },
|
|
], total: 2, pages: 1 })
|
|
const wrapper = await mountAccountsView(['platform:finance:customer-account:list', 'platform:finance:recharge:review'], '/finance/customer-accounts/10001')
|
|
const requestText = wrapper.find('.recharge-table').text()
|
|
expect(requestText.indexOf('RC-91')).toBeLessThan(requestText.indexOf('RC-92'))
|
|
expect(wrapper.find('.recharge-table').findAll('button').map(button => button.text()).filter(text => text === '审核')).toEqual(['审核'])
|
|
|
|
const vm = wrapper.vm as unknown as { reviewForm: Record<string, string>; openReview(request: Record<string, unknown>): void; submitReview(): Promise<void> }
|
|
vm.openReview({ requestId: '91', requestNo: 'RC-91', amount: '500.00', voucherFileId: '81', voucherFileName: 'receipt.png', status: 'PENDING', version: 3, submittedByName: '客户A', reviewedByName: '', reviewRemark: '', createdAt: '2026-09-04T10:00:00', reviewedAt: '' })
|
|
Object.assign(vm.reviewForm, { decision: 'REJECT', remark: ' 凭证不清晰 ' })
|
|
await vm.submitReview()
|
|
expect(reviewFinanceRechargeRequest).toHaveBeenCalledWith('91', { decision: 'REJECT', remark: '凭证不清晰', expectedVersion: 3 })
|
|
expect(listFinanceRechargeRequests).toHaveBeenCalledTimes(2)
|
|
expect(listFinanceAccountLedger).toHaveBeenCalledTimes(2)
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('creates account and ledger export tasks from the complete active filters without page parameters', async () => {
|
|
const permissions = ['platform:finance:customer-account:list', 'platform:finance:customer-account:export', 'platform:download:list']
|
|
const listWrapper = await mountAccountsView(permissions)
|
|
const listVm = listWrapper.vm as unknown as { filters: Record<string, unknown>; exportAccounts(): Promise<void> }
|
|
Object.assign(listVm.filters, { customerId: '10001', status: 'ACTIVE' })
|
|
await listVm.exportAccounts()
|
|
expect(createPlatformExportTask).toHaveBeenLastCalledWith({
|
|
exportType: 'FINANCE_CUSTOMER_ACCOUNT_LIST', format: 'XLSX', taskName: '客户账户',
|
|
parameters: { customerId: '10001', status: 'ACTIVE' },
|
|
})
|
|
expect(createPlatformExportTask).not.toHaveBeenCalledWith(expect.objectContaining({ parameters: expect.objectContaining({ page: expect.anything() }) }))
|
|
listWrapper.unmount()
|
|
|
|
vi.mocked(createPlatformExportTask).mockResolvedValueOnce({ id: 'exp-2', taskNo: 'EXP-2', taskName: '客户账户流水', status: 'PENDING' } as never)
|
|
const detailWrapper = await mountAccountsView(permissions, '/finance/customer-accounts/10001')
|
|
const detailVm = detailWrapper.vm as unknown as { ledgerFilters: Record<string, unknown>; exportLedger(): Promise<void> }
|
|
Object.assign(detailVm.ledgerFilters, { businessType: 'RECEIPT', occurredRange: ['2026-08-01', '2026-08-31'] })
|
|
await detailVm.exportLedger()
|
|
expect(createPlatformExportTask).toHaveBeenLastCalledWith({
|
|
exportType: 'FINANCE_CUSTOMER_ACCOUNT_LEDGER', format: 'XLSX', taskName: '客户账户流水',
|
|
parameters: { customerId: '10001', businessType: 'RECEIPT', startDate: '2026-08-01', endDate: '2026-08-31' },
|
|
})
|
|
expect(showExportTaskCreatedDialog).toHaveBeenLastCalledWith({ taskName: '客户账户流水', taskNo: 'EXP-2', dense: true })
|
|
detailWrapper.unmount()
|
|
})
|
|
|
|
it('loads voucher images through the authenticated blob endpoint', async () => {
|
|
const wrapper = await mountAccountsView(['platform:finance:customer-account:list'], '/finance/customer-accounts/10001')
|
|
const voucherButton = wrapper.findAll('.recharge-table button').find(button => button.text() === 'receipt.png')
|
|
expect(voucherButton).toBeTruthy()
|
|
await voucherButton!.trigger('click')
|
|
await flushPromises()
|
|
expect(downloadFinanceRechargeVoucher).toHaveBeenCalledWith('91')
|
|
expect(URL.createObjectURL).toHaveBeenCalled()
|
|
expect(wrapper.find('.voucher-preview__image').attributes('src')).toBe('blob:finance-voucher')
|
|
wrapper.unmount()
|
|
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:finance-voucher')
|
|
})
|
|
|
|
it('requires both the finance export permission and download-center access', async () => {
|
|
const wrapper = await mountAccountsView(['platform:finance:customer-account:list', 'platform:finance:customer-account:export'])
|
|
expect(wrapper.findAll('button').map((button) => button.text())).not.toContain('导出Excel')
|
|
expect(createPlatformExportTask).not.toHaveBeenCalled()
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('opens the download center after confirmation and reports task creation failures', async () => {
|
|
const permissions = ['platform:finance:customer-account:list', 'platform:finance:customer-account:export', 'platform:download:list']
|
|
vi.mocked(showExportTaskCreatedDialog).mockResolvedValueOnce(true)
|
|
const wrapper = await mountAccountsView(permissions)
|
|
await (wrapper.vm as unknown as { exportAccounts(): Promise<void> }).exportAccounts()
|
|
expect(wrapper.router.currentRoute.value.name).toBe('download-center')
|
|
wrapper.unmount()
|
|
|
|
const errorSpy = vi.spyOn(ElMessage, 'error').mockImplementation(() => undefined as never)
|
|
vi.mocked(createPlatformExportTask).mockRejectedValueOnce(new Error('导出服务不可用'))
|
|
const failedWrapper = await mountAccountsView(permissions)
|
|
await (failedWrapper.vm as unknown as { exportAccounts(): Promise<void> }).exportAccounts()
|
|
expect(errorSpy).toHaveBeenCalledWith('客户账户导出任务创建失败')
|
|
expect(showExportTaskCreatedDialog).toHaveBeenCalledTimes(1)
|
|
failedWrapper.unmount()
|
|
errorSpy.mockRestore()
|
|
})
|
|
})
|
|
|
|
afterAll(() => vi.unstubAllGlobals())
|