230 lines
9.8 KiB
JavaScript
230 lines
9.8 KiB
JavaScript
// 商户中心 —— 发票管理路由:/api/invoice/*(写操作,部分为"透传中台"语义但 mock 实际维护
|
|
// 本地状态以保证 demo 可跑通)+ /api/invoice-management/*(本地查询)。
|
|
import express from 'express'
|
|
import { invoices, repayments } from '../db/seedInvoice.js'
|
|
import { nextId } from '../utils/id.js'
|
|
import { paginate } from '../utils/pagination.js'
|
|
import { sendOk, sendFail } from '../utils/response.js'
|
|
import { requireAuth } from '../middleware/requireAuth.js'
|
|
|
|
const router = express.Router()
|
|
router.use(requireAuth)
|
|
|
|
function nowStr() {
|
|
return new Date().toISOString().slice(0, 19).replace('T', ' ')
|
|
}
|
|
|
|
function recalcInvoiceStatus(invoice) {
|
|
const matched = Number(invoice.matchedAmount || 0)
|
|
const total = Number(invoice.invoiceAmount || 0)
|
|
if (matched <= 0) invoice.matchStatus = 'UNMATCHED'
|
|
else if (matched >= total) invoice.matchStatus = 'MATCHED'
|
|
else invoice.matchStatus = 'PARTIAL_MATCH'
|
|
}
|
|
|
|
function recalcRepaymentStatus(repayment) {
|
|
const matched = Number(repayment.matchedAmount || 0)
|
|
const total = Number(repayment.repaymentAmount || 0)
|
|
if (repayment.status === 'SETTLED') return
|
|
repayment.status = matched >= total ? 'MATCHED' : 'PENDING'
|
|
}
|
|
|
|
// ---------------- /api/invoice/* (写操作) ----------------
|
|
router.post('/invoice/create', (req, res) => {
|
|
const body = req.body || {}
|
|
if (!body.merchantId) return sendFail(res, '所属商户不能为空', 400)
|
|
if (!body.invoiceCode) return sendFail(res, '发票代码不能为空', 400)
|
|
if (!body.invoiceNo) return sendFail(res, '发票号码不能为空', 400)
|
|
if (!body.invoiceType) return sendFail(res, '发票类型不能为空', 400)
|
|
if (!body.invoiceAmount) return sendFail(res, '发票金额不能为空', 400)
|
|
if (invoices.some((inv) => inv.invoiceCode === body.invoiceCode && inv.invoiceNo === body.invoiceNo)) {
|
|
return sendFail(res, '该发票已登记,请勿重复上传', 400)
|
|
}
|
|
const now = nowStr()
|
|
const entity = {
|
|
...body,
|
|
id: nextId('invoice'),
|
|
matchStatus: 'UNMATCHED',
|
|
matchedAmount: '0.00',
|
|
settleStatus: 'UNSETTLED',
|
|
settledAmount: '0.00',
|
|
matchRecordList: [],
|
|
createdAt: now,
|
|
updatedAt: now
|
|
}
|
|
invoices.push(entity)
|
|
sendOk(res, entity, '发票登记成功')
|
|
})
|
|
|
|
router.post('/invoice/batch-create', (req, res) => {
|
|
const list = Array.isArray(req.body) ? req.body : []
|
|
let successCount = 0
|
|
const failureDetails = []
|
|
const now = nowStr()
|
|
list.forEach((body, index) => {
|
|
if (!body.invoiceCode || !body.invoiceNo || !body.invoiceType || !body.invoiceAmount) {
|
|
failureDetails.push(`第${index + 1}行:必填字段缺失`)
|
|
return
|
|
}
|
|
if (invoices.some((inv) => inv.invoiceCode === body.invoiceCode && inv.invoiceNo === body.invoiceNo)) {
|
|
failureDetails.push(`第${index + 1}行:发票${body.invoiceNo}已登记,请勿重复上传`)
|
|
return
|
|
}
|
|
invoices.push({
|
|
...body,
|
|
id: nextId('invoice'),
|
|
matchStatus: 'UNMATCHED',
|
|
matchedAmount: '0.00',
|
|
settleStatus: 'UNSETTLED',
|
|
settledAmount: '0.00',
|
|
matchRecordList: [],
|
|
createdAt: now,
|
|
updatedAt: now
|
|
})
|
|
successCount += 1
|
|
})
|
|
sendOk(
|
|
res,
|
|
{
|
|
totalCount: list.length,
|
|
successCount,
|
|
failCount: list.length - successCount,
|
|
failureDetails: failureDetails.join(';')
|
|
},
|
|
'批量登记完成'
|
|
)
|
|
})
|
|
|
|
router.post('/invoice/match', (req, res) => {
|
|
const { channelNo, invoiceNo, data } = req.body || {}
|
|
if (!channelNo) return sendFail(res, '所属渠道不能为空', 400)
|
|
const invoice = invoices.find((inv) => inv.invoiceNo === invoiceNo)
|
|
if (!invoice) return sendFail(res, '发票不存在', 404)
|
|
if (invoice.invoiceStatus !== 'ACTIVE') return sendFail(res, '该发票已作废,不可匹配', 400)
|
|
if (!Array.isArray(data) || !data.length) return sendFail(res, '匹配数据不能为空', 400)
|
|
|
|
const now = nowStr()
|
|
data.forEach((item) => {
|
|
const repayment = repayments.find((r) => r.repaymentSerialNo === item.depositSerialNo)
|
|
const matchAmount = Number(item.matchAmount || 0)
|
|
invoice.matchRecordList = invoice.matchRecordList || []
|
|
invoice.matchRecordList.push({
|
|
id: nextId('invoiceMatchRecord'),
|
|
repaymentSerialNo: item.depositSerialNo,
|
|
repaymentAmount: repayment ? repayment.repaymentAmount : '0.00',
|
|
matchAmount: matchAmount.toFixed(2),
|
|
matchType: 'manual',
|
|
matchTime: now,
|
|
remark: ''
|
|
})
|
|
invoice.matchedAmount = (Number(invoice.matchedAmount || 0) + matchAmount).toFixed(2)
|
|
if (repayment) {
|
|
repayment.matchedAmount = (Number(repayment.matchedAmount || 0) + matchAmount).toFixed(2)
|
|
recalcRepaymentStatus(repayment)
|
|
}
|
|
})
|
|
recalcInvoiceStatus(invoice)
|
|
invoice.updatedAt = now
|
|
sendOk(res, true, '发票匹配成功')
|
|
})
|
|
|
|
router.post('/invoice/settle', (req, res) => {
|
|
const { channelNo, invoiceNo } = req.body || {}
|
|
if (!channelNo) return sendFail(res, '所属渠道不能为空', 400)
|
|
const invoice = invoices.find((inv) => inv.invoiceNo === invoiceNo)
|
|
if (!invoice) return sendFail(res, '发票不存在', 404)
|
|
if (invoice.invoiceStatus !== 'ACTIVE') return sendFail(res, '该发票已作废,不可清算', 400)
|
|
if (invoice.settleStatus === 'SETTLED') return sendFail(res, '该发票已结算,请勿重复清算', 400)
|
|
invoice.settleStatus = Number(invoice.matchedAmount || 0) >= Number(invoice.invoiceAmount || 0) ? 'SETTLED' : 'PARTIAL_SETTLED'
|
|
invoice.settledAmount = invoice.matchedAmount
|
|
invoice.updatedAt = nowStr()
|
|
;(repayments || []).forEach((r) => {
|
|
if (invoice.matchRecordList?.some((m) => m.repaymentSerialNo === r.repaymentSerialNo)) {
|
|
r.status = 'SETTLED'
|
|
}
|
|
})
|
|
sendOk(res, true, '发票清算成功')
|
|
})
|
|
|
|
router.post('/invoice/delete', (req, res) => {
|
|
const { channelNo, invoiceNo } = req.body || {}
|
|
if (!channelNo) return sendFail(res, '所属渠道不能为空', 400)
|
|
const invoice = invoices.find((inv) => inv.invoiceNo === invoiceNo)
|
|
if (!invoice) return sendFail(res, '发票不存在', 404)
|
|
if (invoice.matchStatus !== 'UNMATCHED') return sendFail(res, '该发票已匹配,不可作废', 400)
|
|
invoice.invoiceStatus = 'CANCELLED'
|
|
invoice.updatedAt = nowStr()
|
|
sendOk(res, true, '发票作废成功')
|
|
})
|
|
|
|
router.post('/invoice/unmatch', (req, res) => {
|
|
const { matchRecordId } = req.body || {}
|
|
const invoice = invoices.find((inv) => (inv.matchRecordList || []).some((m) => m.id === matchRecordId))
|
|
if (!invoice) return sendFail(res, '匹配记录不存在', 404)
|
|
if (invoice.settleStatus !== 'UNSETTLED') return sendFail(res, '已进入清算流程的匹配记录不可取消', 400)
|
|
const record = invoice.matchRecordList.find((m) => m.id === matchRecordId)
|
|
invoice.matchRecordList = invoice.matchRecordList.filter((m) => m.id !== matchRecordId)
|
|
invoice.matchedAmount = Math.max(0, Number(invoice.matchedAmount || 0) - Number(record.matchAmount || 0)).toFixed(2)
|
|
recalcInvoiceStatus(invoice)
|
|
invoice.updatedAt = nowStr()
|
|
const repayment = repayments.find((r) => r.repaymentSerialNo === record.repaymentSerialNo)
|
|
if (repayment) {
|
|
repayment.matchedAmount = Math.max(0, Number(repayment.matchedAmount || 0) - Number(record.matchAmount || 0)).toFixed(2)
|
|
recalcRepaymentStatus(repayment)
|
|
}
|
|
sendOk(res, true, '已取消匹配')
|
|
})
|
|
|
|
// ---------------- /api/invoice-management/* (本地查询) ----------------
|
|
router.post('/invoice-management/invoice/list', (req, res) => {
|
|
const {
|
|
merchantIdIs,
|
|
invoiceNoLike,
|
|
registerDateStart,
|
|
registerDateEnd,
|
|
invoiceStatusList,
|
|
matchStatusList,
|
|
settleStatusList,
|
|
accountNoLike,
|
|
oppAccountNoLike,
|
|
amountMin,
|
|
amountMax,
|
|
page,
|
|
pageSize
|
|
} = req.body || {}
|
|
let list = invoices
|
|
if (merchantIdIs) list = list.filter((inv) => inv.merchantId === merchantIdIs)
|
|
if (invoiceNoLike) list = list.filter((inv) => inv.invoiceNo.includes(invoiceNoLike))
|
|
if (registerDateStart) list = list.filter((inv) => inv.registerDate >= registerDateStart)
|
|
if (registerDateEnd) list = list.filter((inv) => inv.registerDate <= registerDateEnd)
|
|
if (invoiceStatusList?.length) list = list.filter((inv) => invoiceStatusList.includes(inv.invoiceStatus))
|
|
if (matchStatusList?.length) list = list.filter((inv) => matchStatusList.includes(inv.matchStatus))
|
|
if (settleStatusList?.length) list = list.filter((inv) => settleStatusList.includes(inv.settleStatus))
|
|
if (accountNoLike) list = list.filter((inv) => inv.accountNo && inv.accountNo.includes(accountNoLike))
|
|
if (oppAccountNoLike) list = list.filter((inv) => inv.oppAccountNo && inv.oppAccountNo.includes(oppAccountNoLike))
|
|
if (amountMin !== undefined && amountMin !== null && amountMin !== '') list = list.filter((inv) => Number(inv.invoiceAmount) >= Number(amountMin))
|
|
if (amountMax !== undefined && amountMax !== null && amountMax !== '') list = list.filter((inv) => Number(inv.invoiceAmount) <= Number(amountMax))
|
|
list = [...list].sort((a, b) => (a.registerDate < b.registerDate ? 1 : -1))
|
|
sendOk(res, paginate(list, page, pageSize))
|
|
})
|
|
|
|
router.post('/invoice-management/invoice/detail', (req, res) => {
|
|
const { id } = req.body || {}
|
|
const invoice = invoices.find((inv) => inv.id === id)
|
|
if (!invoice) return sendFail(res, '发票不存在', 404)
|
|
sendOk(res, invoice)
|
|
})
|
|
|
|
router.post('/invoice-management/repayment/list', (req, res) => {
|
|
const { merchantIdIs, accountNoLike, oppAccountNoLike, timeStart, timeEnd, page, pageSize } = req.body || {}
|
|
let list = repayments
|
|
if (merchantIdIs) list = list.filter((r) => r.merchantId === merchantIdIs)
|
|
if (accountNoLike) list = list.filter((r) => r.accountNo && r.accountNo.includes(accountNoLike))
|
|
if (oppAccountNoLike) list = list.filter((r) => r.oppAccountNo && r.oppAccountNo.includes(oppAccountNoLike))
|
|
if (timeStart) list = list.filter((r) => r.repaymentTime >= timeStart)
|
|
if (timeEnd) list = list.filter((r) => r.repaymentTime <= timeEnd)
|
|
sendOk(res, paginate(list, page, pageSize))
|
|
})
|
|
|
|
export default router
|