SFT/mock-server/routes/invoice.js

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