优化
This commit is contained in:
@@ -11,7 +11,7 @@ import org.apache.ibatis.annotations.Update;
|
|||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntity> {
|
public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntity> {
|
||||||
@Update("UPDATE fl_container_booking SET container_no=#{containerNo},ocean_booking_id=#{oceanBookingId},ocean_booking_requirement_id=#{requirementId},record_status='ACTIVE',bill_of_lading_no=#{billOfLadingNo},vessel_name=#{vesselName},voyage_no=#{voyageNo},shipping_company_id=#{shippingCompanyId},shipping_company_name=#{shippingCompanyName},departure_port_code=#{departurePortCode},departure_port_name=#{departurePortName},destination_port_code=#{destinationPortCode},destination_port_name=#{destinationPortName},channel_code=#{channelCode},channel_name=#{channelName},customer_id=#{customerId},customer_name=#{customerName},estimated_sailing_at=#{estimatedSailingAt},estimated_port_arrival_at=#{estimatedArrivalAt},updated_by=#{operatorId},updated_by_name=#{operatorName},updated_at=NOW(),version=version+1 WHERE id=#{id} AND version=#{version} AND source_type='SIMULATION' AND record_status='ACTIVE' AND ocean_booking_id IS NULL")
|
@Update("UPDATE fl_container_booking SET container_no=#{containerNo},ocean_booking_id=#{oceanBookingId},ocean_booking_requirement_id=#{requirementId},record_status='ACTIVE',bill_of_lading_no=#{billOfLadingNo},vessel_name=#{vesselName},voyage_no=#{voyageNo},shipping_company_id=#{shippingCompanyId},shipping_company_name=#{shippingCompanyName},departure_port_code=#{departurePortCode},departure_port_name=#{departurePortName},destination_port_code=#{destinationPortCode},destination_port_name=#{destinationPortName},channel_code=#{channelCode},channel_name=#{channelName},customer_id=#{customerId},customer_name=#{customerName},estimated_sailing_at=#{estimatedSailingAt},estimated_port_arrival_at=#{estimatedPortArrivalAt},updated_by=#{operatorId},updated_by_name=#{operatorName},updated_at=NOW(),version=version+1 WHERE id=#{id} AND version=#{version} AND source_type='SIMULATION' AND record_status='ACTIVE' AND ocean_booking_id IS NULL")
|
||||||
int linkReleasedBooking(@Param("id") long id, @Param("oceanBookingId") long oceanBookingId, @Param("requirementId") long requirementId, @Param("containerNo") String containerNo, @Param("billOfLadingNo") String billOfLadingNo, @Param("vesselName") String vesselName, @Param("voyageNo") String voyageNo, @Param("shippingCompanyId") Long shippingCompanyId, @Param("shippingCompanyName") String shippingCompanyName, @Param("departurePortCode") String departurePortCode, @Param("departurePortName") String departurePortName, @Param("destinationPortCode") String destinationPortCode, @Param("destinationPortName") String destinationPortName, @Param("channelCode") String channelCode, @Param("channelName") String channelName, @Param("customerId") Long customerId, @Param("customerName") String customerName, @Param("estimatedSailingAt") java.time.LocalDateTime estimatedSailingAt, @Param("estimatedPortArrivalAt") java.time.LocalDateTime estimatedPortArrivalAt, @Param("version") int version, @Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
|
int linkReleasedBooking(@Param("id") long id, @Param("oceanBookingId") long oceanBookingId, @Param("requirementId") long requirementId, @Param("containerNo") String containerNo, @Param("billOfLadingNo") String billOfLadingNo, @Param("vesselName") String vesselName, @Param("voyageNo") String voyageNo, @Param("shippingCompanyId") Long shippingCompanyId, @Param("shippingCompanyName") String shippingCompanyName, @Param("departurePortCode") String departurePortCode, @Param("departurePortName") String departurePortName, @Param("destinationPortCode") String destinationPortCode, @Param("destinationPortName") String destinationPortName, @Param("channelCode") String channelCode, @Param("channelName") String channelName, @Param("customerId") Long customerId, @Param("customerName") String customerName, @Param("estimatedSailingAt") java.time.LocalDateTime estimatedSailingAt, @Param("estimatedPortArrivalAt") java.time.LocalDateTime estimatedPortArrivalAt, @Param("version") int version, @Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
|
||||||
@Select("""
|
@Select("""
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
+18
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableName;
|
|||||||
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||||
import com.baiyue.firstleg.mapper.ContainerBookingMapper;
|
import com.baiyue.firstleg.mapper.ContainerBookingMapper;
|
||||||
import org.apache.ibatis.annotations.Select;
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
import org.apache.ibatis.annotations.Update;
|
import org.apache.ibatis.annotations.Update;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -61,6 +62,23 @@ class ContainerBookingMapperContractTest {
|
|||||||
assertThat(sql).contains("i.account_id<>#{customerId}").doesNotContain("<>");
|
assertThat(sql).contains("i.account_id<>#{customerId}").doesNotContain("<>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("关联订舱 SQL 的预计到港参数与 Mapper 参数名一致")
|
||||||
|
void linkReleasedBookingUsesDeclaredEstimatedArrivalParameter() {
|
||||||
|
var method = Arrays.stream(ContainerBookingMapper.class.getDeclaredMethods())
|
||||||
|
.filter(candidate -> candidate.getName().equals("linkReleasedBooking"))
|
||||||
|
.findFirst().orElseThrow();
|
||||||
|
String sql = String.join("\n", method.getAnnotation(Update.class).value());
|
||||||
|
var parameterNames = Arrays.stream(method.getParameters())
|
||||||
|
.map(parameter -> parameter.getAnnotation(Param.class).value())
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
assertThat(parameterNames).contains("estimatedPortArrivalAt");
|
||||||
|
assertThat(sql)
|
||||||
|
.contains("estimated_port_arrival_at=#{estimatedPortArrivalAt}")
|
||||||
|
.doesNotContain("#{estimatedArrivalAt}");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("草稿完整更新和预装运输白名单都带版本与状态门禁")
|
@DisplayName("草稿完整更新和预装运输白名单都带版本与状态门禁")
|
||||||
void guardsUpdatesInSql() {
|
void guardsUpdatesInSql() {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ test('验收只使用本轮 marker fixture,解析真实 XLSX 并覆盖门禁
|
|||||||
assert.match(source, /Provider 失败信息可见/)
|
assert.match(source, /Provider 失败信息可见/)
|
||||||
assert.match(source, /9223372036854775806/)
|
assert.match(source, /9223372036854775806/)
|
||||||
assert.doesNotMatch(source, /exportType:\s*['"]CUSTOMER_LIST/)
|
assert.doesNotMatch(source, /exportType:\s*['"]CUSTOMER_LIST/)
|
||||||
|
assert.doesNotMatch(source, /FINANCE_FEE_SOURCE_LIST:[^\n]*参与结算/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('未部署时能力探测后记录部署前置,不借历史业务数据', async () => {
|
test('未部署时能力探测后记录部署前置,不借历史业务数据', async () => {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const REQUIRED_PERMISSIONS = Object.freeze([
|
|||||||
export const EXPORT_SPECS = Object.freeze({
|
export const EXPORT_SPECS = Object.freeze({
|
||||||
FINANCE_CUSTOMER_ACCOUNT_LIST: ['客户编码', '客户', '联系人', '联系电话', '待收(CNY)', '待开账(CNY)', '本期已收(CNY)', '待付(CNY)', '客户状态', '最近变动'],
|
FINANCE_CUSTOMER_ACCOUNT_LIST: ['客户编码', '客户', '联系人', '联系电话', '待收(CNY)', '待开账(CNY)', '本期已收(CNY)', '待付(CNY)', '客户状态', '最近变动'],
|
||||||
FINANCE_CUSTOMER_ACCOUNT_LEDGER: ['流水号', '发生时间', '业务类型', '关联单据', '应收变动(CNY)', '应付变动(CNY)', '变动后未收(CNY)', '操作人', '备注'],
|
FINANCE_CUSTOMER_ACCOUNT_LEDGER: ['流水号', '发生时间', '业务类型', '关联单据', '应收变动(CNY)', '应付变动(CNY)', '变动后未收(CNY)', '操作人', '备注'],
|
||||||
FINANCE_FEE_SOURCE_LIST: ['客户', '收付方向', '来源类型', '来源单据ID', '来源版本', '入仓号', '柜次号', '业务发生日', '费用项目', '计费量', '单价(CNY)', '金额(CNY)', '审核状态', '参与结算', '来源状态', '已生成对账单'],
|
FINANCE_FEE_SOURCE_LIST: ['客户', '收付方向', '来源类型', '来源单据ID', '来源版本', '入仓号', '柜次号', '业务发生日', '费用项目', '计费量', '单价(CNY)', '金额(CNY)', '审核状态', '来源状态', '已生成对账单'],
|
||||||
FINANCE_RECONCILIATION_STATEMENT_LIST: ['对账单号', '客户', '结算起始日', '结算结束日', '账期(天)', '货币', '总应收金额(CNY)', '已收金额(CNY)', '未收金额(CNY)', '是否结清', '审核状态', '备注', '创建人', '创建时间', '更新人', '更新时间'],
|
FINANCE_RECONCILIATION_STATEMENT_LIST: ['对账单号', '客户', '结算起始日', '结算结束日', '账期(天)', '货币', '总应收金额(CNY)', '已收金额(CNY)', '未收金额(CNY)', '是否结清', '审核状态', '备注', '创建人', '创建时间', '更新人', '更新时间'],
|
||||||
FINANCE_RECEIVABLE_SETTLEMENT_LIST: ['结算单号', '客户', '供应商', '货币', '总金额(CNY)', '已收/付金额(CNY)', '未收/付金额(CNY)', '业务期间', '生成日期', '到期日期', '状态'],
|
FINANCE_RECEIVABLE_SETTLEMENT_LIST: ['结算单号', '客户', '供应商', '货币', '总金额(CNY)', '已收/付金额(CNY)', '未收/付金额(CNY)', '业务期间', '生成日期', '到期日期', '状态'],
|
||||||
FINANCE_RECEIVABLE_SETTLEMENT_DETAIL: ['结算单号', '客户', '供应商', '费用项目', '入仓号', '柜次号', '业务发生日', '明细金额(CNY)', '结算总额(CNY)', '已收/付金额(CNY)', '未收/付金额(CNY)', '状态'],
|
FINANCE_RECEIVABLE_SETTLEMENT_DETAIL: ['结算单号', '客户', '供应商', '费用项目', '入仓号', '柜次号', '业务发生日', '明细金额(CNY)', '结算总额(CNY)', '已收/付金额(CNY)', '未收/付金额(CNY)', '状态'],
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ function ok(data) {
|
|||||||
|
|
||||||
function fakeContext(withFixture = true, permissions = financePermissions, denyFinanceWrites = false,
|
function fakeContext(withFixture = true, permissions = financePermissions, denyFinanceWrites = false,
|
||||||
missingSubject = false, omitPayableSource = false) {
|
missingSubject = false, omitPayableSource = false) {
|
||||||
|
process.env.MERCHANT_TEST_USERNAME = 'fixture-client'
|
||||||
|
process.env.MERCHANT_TEST_PASSWORD = 'fixture-password'
|
||||||
const marker = 'FIN-TEST-MARKER'
|
const marker = 'FIN-TEST-MARKER'
|
||||||
const receivables = [
|
const receivables = [
|
||||||
{ feeSourceId: '101', orderId: '201', direction: 'RECEIVABLE', currency: 'CNY', businessDate: '2026-08-10' },
|
{ feeSourceId: '101', orderId: '201', direction: 'RECEIVABLE', currency: 'CNY', businessDate: '2026-08-10' },
|
||||||
@@ -75,6 +77,8 @@ function fakeContext(withFixture = true, permissions = financePermissions, denyF
|
|||||||
line.adjustmentReason = adjustment.reason
|
line.adjustmentReason = adjustment.reason
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (body.action === 'ISSUE') statement.customerConfirmationStatus = 'PENDING_CONFIRMATION'
|
||||||
|
if (body.action === 'VOID' || body.action === 'REOPEN') statement.customerConfirmationStatus = 'NOT_REQUIRED'
|
||||||
return responseForStatement(statement)
|
return responseForStatement(statement)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,6 +92,9 @@ function fakeContext(withFixture = true, permissions = financePermissions, denyF
|
|||||||
if (method === 'GET' && requestPath === '/api/platform/v1/iam/authorization/permissions') {
|
if (method === 'GET' && requestPath === '/api/platform/v1/iam/authorization/permissions') {
|
||||||
return ok(permissions)
|
return ok(permissions)
|
||||||
}
|
}
|
||||||
|
if (method === 'POST' && requestPath === '/api/auth/login') {
|
||||||
|
return ok({ accessToken: 'client-token' })
|
||||||
|
}
|
||||||
if (method === 'GET' && requestPath === '/api/platform/finance/v1/accounts?page=1&pageSize=20&customerId=11') {
|
if (method === 'GET' && requestPath === '/api/platform/finance/v1/accounts?page=1&pageSize=20&customerId=11') {
|
||||||
return ok({ records: [{ customerId: '11', customerCode: 'TEN11', customerName: 'Fixture Account',
|
return ok({ records: [{ customerId: '11', customerCode: 'TEN11', customerName: 'Fixture Account',
|
||||||
contactName: 'Fixture Contact', contactPhone: '13800000000', outstandingReceivable: 100,
|
contactName: 'Fixture Contact', contactPhone: '13800000000', outstandingReceivable: 100,
|
||||||
@@ -182,6 +189,23 @@ function fakeContext(withFixture = true, permissions = financePermissions, denyF
|
|||||||
createdBy: item.createdBy, createdAt: item.createdAt, updatedBy: item.updatedBy, updatedAt: item.updatedAt,
|
createdBy: item.createdBy, createdAt: item.createdAt, updatedBy: item.updatedBy, updatedAt: item.updatedAt,
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
if (method === 'GET' && requestPath === '/api/client/finance/v1/statements') {
|
||||||
|
return ok([...statements.values()].map((item) => ({
|
||||||
|
statementId: item.statementId, customerId: item.customerId, status: item.status,
|
||||||
|
version: item.version, customerConfirmationStatus: item.customerConfirmationStatus,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
const clientConfirmMatch = requestPath.match(/\/api\/client\/finance\/v1\/statements\/(\d+)\/confirm$/)
|
||||||
|
if (method === 'POST' && clientConfirmMatch) {
|
||||||
|
const statement = statements.get(clientConfirmMatch[1])
|
||||||
|
statement.customerConfirmationStatus = 'CONFIRMED'
|
||||||
|
statement.version += 1
|
||||||
|
const result = { statementId: statement.statementId, confirmationStatus: 'CONFIRMED',
|
||||||
|
confirmedBy: '7002', confirmedByName: 'fixture-client', confirmedAt: '2026-08-10T10:00:00',
|
||||||
|
version: statement.version }
|
||||||
|
if (idem) idempotency.set(idem, result)
|
||||||
|
return ok(structuredClone(result))
|
||||||
|
}
|
||||||
const detailMatch = requestPath.match(/\/reconciliation-statements\/(\d+)$/)
|
const detailMatch = requestPath.match(/\/reconciliation-statements\/(\d+)$/)
|
||||||
if (method === 'GET' && detailMatch) return ok(responseForStatement(statements.get(detailMatch[1])))
|
if (method === 'GET' && detailMatch) return ok(responseForStatement(statements.get(detailMatch[1])))
|
||||||
throw new Error(`Unexpected request: ${method} ${requestPath}`)
|
throw new Error(`Unexpected request: ${method} ${requestPath}`)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const MODULE = 'finance-reconciliation-full'
|
const MODULE = 'finance-reconciliation-full'
|
||||||
const SCENARIO = 'finance-full-reconciliation'
|
const SCENARIO = 'finance-full-reconciliation'
|
||||||
const BASE = '/api/platform/finance/v1'
|
const BASE = '/api/platform/finance/v1'
|
||||||
|
const CLIENT_BASE = '/api/client/finance/v1'
|
||||||
const OPTIONS = { scenario: SCENARIO, deploymentBlocker: true }
|
const OPTIONS = { scenario: SCENARIO, deploymentBlocker: true }
|
||||||
const STATEMENT_LIST_PERMISSION = 'platform:finance:customer-statement:list'
|
const STATEMENT_LIST_PERMISSION = 'platform:finance:customer-statement:list'
|
||||||
const LEGACY_STATEMENT_VIEW_PERMISSION = 'platform:finance:bill:view'
|
const LEGACY_STATEMENT_VIEW_PERMISSION = 'platform:finance:bill:view'
|
||||||
@@ -119,6 +120,51 @@ function finish(context, start) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function confirmIssuedStatementAsClient(context, fixture, statement) {
|
||||||
|
const username = process.env.MERCHANT_TEST_USERNAME
|
||||||
|
const password = process.env.MERCHANT_TEST_PASSWORD
|
||||||
|
const targetId = statementId(statement)
|
||||||
|
if (!username || !password || !targetId) {
|
||||||
|
context.assert(MODULE, '客户确认对账单凭据与单据门禁', null, false,
|
||||||
|
`username=${username ? 'present' : 'missing'}, password=${password ? 'present' : 'missing'}, statementId=${targetId || 'missing'}`, OPTIONS)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const loginResponse = await context.request('POST', '/api/auth/login', { username, password }, '')
|
||||||
|
const login = context.apiSuccess(MODULE, 'Client 客户登录用于确认对账单', loginResponse,
|
||||||
|
`username=${username}`, OPTIONS)
|
||||||
|
const clientToken = login?.accessToken
|
||||||
|
if (!clientToken) return null
|
||||||
|
const listResponse = await context.request('GET', `${CLIENT_BASE}/statements`, undefined, clientToken)
|
||||||
|
const statements = context.apiSuccess(MODULE, 'Client 查询本企业对账单', listResponse,
|
||||||
|
`customerId=${fixture.customerId}, statementId=${targetId}`, OPTIONS)
|
||||||
|
const before = rows(statements).find((item) => statementId(item) === targetId)
|
||||||
|
context.assert(MODULE, 'Client 仅取得本企业待确认已出账对账单', listResponse,
|
||||||
|
id(before?.customerId) === fixture.customerId && before?.status === 'ISSUED'
|
||||||
|
&& before?.customerConfirmationStatus === 'PENDING_CONFIRMATION',
|
||||||
|
`statementId=${statementId(before) || 'missing'}, status=${before?.status ?? 'missing'}, confirmation=${before?.customerConfirmationStatus ?? 'missing'}`, OPTIONS)
|
||||||
|
if (!before) return null
|
||||||
|
const headers = { 'Idempotency-Key': key(context, `client-confirm-${targetId}`) }
|
||||||
|
const body = { expectedVersion: Number(before.version) }
|
||||||
|
const confirmResponse = await context.request('POST',
|
||||||
|
`${CLIENT_BASE}/statements/${encodeURIComponent(targetId)}/confirm`, body, clientToken, headers)
|
||||||
|
const confirmed = context.apiSuccess(MODULE, 'Client 确认本人已出账对账单', confirmResponse,
|
||||||
|
`statementId=${targetId}`, OPTIONS)
|
||||||
|
context.assert(MODULE, '客户确认状态、确认人和版本已落地', confirmResponse,
|
||||||
|
statementId(confirmed) === targetId && confirmed?.confirmationStatus === 'CONFIRMED'
|
||||||
|
&& present(confirmed?.confirmedBy) && present(confirmed?.confirmedByName)
|
||||||
|
&& present(confirmed?.confirmedAt) && Number(confirmed?.version) === Number(before.version) + 1,
|
||||||
|
`statementId=${statementId(confirmed) || 'missing'}, confirmation=${confirmed?.confirmationStatus ?? 'missing'}, version=${confirmed?.version ?? 'missing'}`, OPTIONS)
|
||||||
|
const replayResponse = await context.request('POST',
|
||||||
|
`${CLIENT_BASE}/statements/${encodeURIComponent(targetId)}/confirm`, body, clientToken, headers)
|
||||||
|
const replay = context.apiSuccess(MODULE, 'Client 客户确认幂等重放', replayResponse,
|
||||||
|
`statementId=${targetId}`, OPTIONS)
|
||||||
|
context.assert(MODULE, '客户确认幂等不重复递增版本', replayResponse,
|
||||||
|
statementId(replay) === targetId && Number(replay?.version) === Number(confirmed?.version),
|
||||||
|
`firstVersion=${confirmed?.version ?? 'missing'}, replayVersion=${replay?.version ?? 'missing'}`, OPTIONS)
|
||||||
|
fixture.root.clientConfirmation = confirmed
|
||||||
|
return confirmed
|
||||||
|
}
|
||||||
|
|
||||||
async function approveFixtureSources(context, fixture) {
|
async function approveFixtureSources(context, fixture) {
|
||||||
const approved = []
|
const approved = []
|
||||||
for (const source of fixture.sources) {
|
for (const source of fixture.sources) {
|
||||||
@@ -539,6 +585,8 @@ export async function run(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await confirmIssuedStatementAsClient(context, fixture, issuedStatements[0])
|
||||||
|
|
||||||
const receivableSettlements = []
|
const receivableSettlements = []
|
||||||
for (let index = 0; index < issuedStatements.length; index += 1) {
|
for (let index = 0; index < issuedStatements.length; index += 1) {
|
||||||
const settlement = await createReceivable(context, issuedStatements[index], index + 1)
|
const settlement = await createReceivable(context, issuedStatements[index], index + 1)
|
||||||
|
|||||||
Reference in New Issue
Block a user