docs/test(P19ERP3-5,P19ERP3-6): 收口资产ERP文档矩阵与回归脚本
这个提交包含在:
@@ -271,6 +271,54 @@ const server = http.createServer(async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && url.pathname === '/erp/assets/export') {
|
||||
const mode = url.searchParams.get('mode') || 'success';
|
||||
const chunks = [];
|
||||
for await (const chunk of req) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
let parsedBody = null;
|
||||
try {
|
||||
parsedBody = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
|
||||
} catch {
|
||||
parsedBody = null;
|
||||
}
|
||||
|
||||
if (mode === 'timeout') {
|
||||
setTimeout(() => {
|
||||
sendJson(res, 200, {
|
||||
ok: true,
|
||||
externalAssetId: `ERP-ASSET-${Date.now()}`,
|
||||
externalAssetNo: `ERP-ASSET-${parsedBody?.assetNo || Date.now()}`,
|
||||
});
|
||||
}, 1500);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === 'client') {
|
||||
return sendJson(res, 422, {
|
||||
ok: false,
|
||||
code: 'invalid_payload',
|
||||
message: 'mock asset client error',
|
||||
});
|
||||
}
|
||||
|
||||
if (mode === 'server') {
|
||||
return sendJson(res, 503, {
|
||||
ok: false,
|
||||
code: 'service_unavailable',
|
||||
message: 'mock asset server error',
|
||||
});
|
||||
}
|
||||
|
||||
return sendJson(res, 200, {
|
||||
ok: true,
|
||||
externalAssetId: `ERP-ASSET-${parsedBody?.assetNo || parsedBody?.entityId || Date.now()}`,
|
||||
externalAssetNo: `ERP-ASSET-${parsedBody?.assetNo || parsedBody?.entityId || Date.now()}`,
|
||||
});
|
||||
}
|
||||
|
||||
sendJson(res, 404, { ok: false, message: 'not_found' });
|
||||
});
|
||||
|
||||
@@ -304,9 +352,11 @@ start_app_server() {
|
||||
ERP_MASTER_DATA_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/master-data/pull?mode=success" \
|
||||
ERP_JOURNAL_EXPORT_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/journals/export?mode=success" \
|
||||
ERP_PROCUREMENT_EXPORT_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/procurement/export?mode=success" \
|
||||
ERP_ASSET_EXPORT_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/assets/export?mode=success" \
|
||||
ERP_SYNC_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/reimbursements/paid?mode=success" \
|
||||
ERP_SYNC_TIMEOUT_MS="$ERP_SYNC_TIMEOUT_MS" \
|
||||
ERP_PROCUREMENT_TIMEOUT_MS="$ERP_SYNC_TIMEOUT_MS" \
|
||||
ERP_ASSET_TIMEOUT_MS="$ERP_SYNC_TIMEOUT_MS" \
|
||||
ERP_MASTER_DATA_TIMEOUT_MS="$ERP_SYNC_TIMEOUT_MS" \
|
||||
ERP_SYNC_AUTH_TOKEN="" \
|
||||
node src/index.js >"$TMP_DIR/app.log" 2>&1 &
|
||||
@@ -404,6 +454,39 @@ const { createPrismaClient } = require('./src/prismaClient');
|
||||
NODE
|
||||
}
|
||||
|
||||
reset_erp_integration_state() {
|
||||
DATABASE_URL="$DATABASE_URL" node <<'NODE'
|
||||
require('dotenv/config');
|
||||
const { createPrismaClient } = require('./src/prismaClient');
|
||||
|
||||
(async () => {
|
||||
const { prisma, pool } = createPrismaClient();
|
||||
await prisma.$connect();
|
||||
try {
|
||||
await prisma.integrationOutbox.deleteMany({
|
||||
where: {
|
||||
event_type: {
|
||||
in: ['erp.reimbursement.paid', 'erp.journal.export', 'erp.procurement.export', 'erp.asset.export'],
|
||||
},
|
||||
},
|
||||
});
|
||||
await prisma.integrationBinding.deleteMany({
|
||||
where: { provider: 'erp' },
|
||||
});
|
||||
await prisma.integrationSyncState.deleteMany({
|
||||
where: { provider: 'erp' },
|
||||
});
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
await pool.end();
|
||||
}
|
||||
})().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
NODE
|
||||
}
|
||||
|
||||
create_paid_reimbursement() {
|
||||
DATABASE_URL="$DATABASE_URL" RUN_SUFFIX="$RUN_SUFFIX" node <<'NODE'
|
||||
require('dotenv/config');
|
||||
@@ -762,7 +845,8 @@ NODE
|
||||
run_outbox_once() {
|
||||
local journal_mode="$1"
|
||||
local procurement_mode="${2:-$journal_mode}"
|
||||
DATABASE_URL="$DATABASE_URL" INTEGRATION_OUTBOX_BATCH_SIZE=500 ERP_SYNC_ENABLED=true ERP_SYNC_PROVIDER=http ERP_JOURNAL_EXPORT_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/journals/export?mode=$journal_mode" ERP_PROCUREMENT_EXPORT_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/procurement/export?mode=$procurement_mode" ERP_SYNC_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/reimbursements/paid?mode=$journal_mode" ERP_SYNC_TIMEOUT_MS="$ERP_SYNC_TIMEOUT_MS" ERP_PROCUREMENT_TIMEOUT_MS="$ERP_SYNC_TIMEOUT_MS" ERP_SYNC_AUTH_TOKEN="" npm run --silent job:integration-outbox-once >"$TMP_DIR/outbox-once.log"
|
||||
local asset_mode="${3:-$journal_mode}"
|
||||
DATABASE_URL="$DATABASE_URL" INTEGRATION_OUTBOX_BATCH_SIZE=500 ERP_SYNC_ENABLED=true ERP_SYNC_PROVIDER=http ERP_JOURNAL_EXPORT_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/journals/export?mode=$journal_mode" ERP_PROCUREMENT_EXPORT_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/procurement/export?mode=$procurement_mode" ERP_ASSET_EXPORT_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/assets/export?mode=$asset_mode" ERP_SYNC_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/reimbursements/paid?mode=$journal_mode" ERP_SYNC_TIMEOUT_MS="$ERP_SYNC_TIMEOUT_MS" ERP_PROCUREMENT_TIMEOUT_MS="$ERP_SYNC_TIMEOUT_MS" ERP_ASSET_TIMEOUT_MS="$ERP_SYNC_TIMEOUT_MS" ERP_SYNC_AUTH_TOKEN="" npm run --silent job:integration-outbox-once >"$TMP_DIR/outbox-once.log"
|
||||
}
|
||||
|
||||
assert_pull_mode() {
|
||||
@@ -863,6 +947,92 @@ archive_purchase_order() {
|
||||
jq -er '.data.archiveOrder.id' "$TMP_DIR/${tmp_prefix}-archive.json"
|
||||
}
|
||||
|
||||
resolve_asset_fixture_ids() {
|
||||
DATABASE_URL="$DATABASE_URL" LEGAL_ENTITY_CODE="$LEGAL_ENTITY_CODE" COST_CENTER_CODE="$COST_CENTER_CODE" node - <<'NODE'
|
||||
require('dotenv/config');
|
||||
const { createPrismaClient } = require('./src/prismaClient');
|
||||
|
||||
(async () => {
|
||||
const { prisma, pool } = createPrismaClient();
|
||||
await prisma.$connect();
|
||||
try {
|
||||
const employee = await prisma.employee.findUnique({
|
||||
where: { email: process.env.SEED_EMPLOYEE_EMAIL ?? '[email protected]' },
|
||||
select: { id: true, legal_entity_id: true },
|
||||
});
|
||||
const primaryCostCenter = await prisma.costCenter.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
archived_at: null,
|
||||
legal_entity_id: employee.legal_entity_id,
|
||||
},
|
||||
orderBy: { created_at: 'asc' },
|
||||
select: { id: true },
|
||||
});
|
||||
const syncedLegalEntity = await prisma.legalEntity.findUnique({
|
||||
where: { code: process.env.LEGAL_ENTITY_CODE },
|
||||
select: { id: true },
|
||||
});
|
||||
const syncedCostCenter = await prisma.costCenter.findFirst({
|
||||
where: {
|
||||
code: process.env.COST_CENTER_CODE,
|
||||
legal_entity_id: syncedLegalEntity?.id ?? undefined,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
process.stdout.write([
|
||||
employee.id,
|
||||
employee.legal_entity_id,
|
||||
primaryCostCenter?.id ?? '',
|
||||
syncedLegalEntity?.id ?? '',
|
||||
syncedCostCenter?.id ?? '',
|
||||
].join('\n'));
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
await pool.end();
|
||||
}
|
||||
})().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
NODE
|
||||
}
|
||||
|
||||
create_asset_category() {
|
||||
local token="$1"
|
||||
local tmp_prefix="$2"
|
||||
local category_code="AST-${RUN_SUFFIX_NUMERIC:0:8}-${tmp_prefix}"
|
||||
local status
|
||||
status="$(api_json_auth POST "/api/asset-categories" "$token" "$TMP_DIR/${tmp_prefix}-asset-category.json" "$(jq -nc --arg code "$category_code" --arg name "资产分类-${tmp_prefix}" '{code:$code, name:$name, enabled:true}')")"
|
||||
assert_status "$status" "200" "create asset category ${tmp_prefix}" "$TMP_DIR/${tmp_prefix}-asset-category.json"
|
||||
jq -er '.data.assetCategory.id' "$TMP_DIR/${tmp_prefix}-asset-category.json"
|
||||
}
|
||||
|
||||
create_asset_card() {
|
||||
local token="$1"
|
||||
local category_id="$2"
|
||||
local legal_entity_id="$3"
|
||||
local cost_center_id="$4"
|
||||
local assignee_employee_id="$5"
|
||||
local tmp_prefix="$6"
|
||||
|
||||
local body
|
||||
body="$(jq -nc \
|
||||
--arg name "资产ERP-${tmp_prefix}" \
|
||||
--arg categoryId "$category_id" \
|
||||
--arg legalEntityId "$legal_entity_id" \
|
||||
--arg costCenterId "$cost_center_id" \
|
||||
--arg assigneeEmployeeId "$assignee_employee_id" \
|
||||
--arg location "上海办公室-A区-${tmp_prefix}" \
|
||||
'{name:$name, categoryId:$categoryId, legalEntityId:$legalEntityId, costCenterId:$costCenterId, assigneeEmployeeId:($assigneeEmployeeId|select(length>0)), location:$location, activatedAt:"2026-03-09"}')"
|
||||
|
||||
local status
|
||||
status="$(api_json_auth POST "/api/assets" "$token" "$TMP_DIR/${tmp_prefix}-asset-create.json" "$body")"
|
||||
assert_status "$status" "200" "create asset ${tmp_prefix}" "$TMP_DIR/${tmp_prefix}-asset-create.json"
|
||||
jq -er '.data.asset.id' "$TMP_DIR/${tmp_prefix}-asset-create.json"
|
||||
}
|
||||
|
||||
assert_export_mode_semantics() {
|
||||
local mode="$1"
|
||||
local state_json="$2"
|
||||
@@ -935,6 +1105,9 @@ run_export_mode_case() {
|
||||
echo "[0/9] Ensure ERP feature flag force_on"
|
||||
set_erp_flag "force_on"
|
||||
|
||||
echo "[0.5/9] Reset ERP integration artifacts"
|
||||
reset_erp_integration_state
|
||||
|
||||
echo "[1/9] Start embedded mock ERP server"
|
||||
start_mock_erp_server
|
||||
|
||||
@@ -1013,6 +1186,13 @@ SUPPLIER_ID="${FIXTURE_IDS[2]}"
|
||||
DOCUMENT_CATEGORY_ID="${FIXTURE_IDS[3]}"
|
||||
BUDGET_POOL_ID="${FIXTURE_IDS[4]}"
|
||||
|
||||
PROC_SUCCESS_PO="PO-P19ERP2-${RUN_SUFFIX_NUMERIC}-001"
|
||||
PROC_SUCCESS_CONTRACT_1="CT-P19ERP2-${RUN_SUFFIX_NUMERIC}-001"
|
||||
PROC_SUCCESS_CONTRACT_2="CT-P19ERP2-${RUN_SUFFIX_NUMERIC}-002"
|
||||
PROC_SUCCESS_CONTRACT_3="CT-P19ERP2-${RUN_SUFFIX_NUMERIC}-003"
|
||||
PROC_SKIPPED_PO="PO-P19ERP2-${RUN_SUFFIX_NUMERIC}-101"
|
||||
PROC_SKIPPED_CONTRACT="CT-P19ERP2-${RUN_SUFFIX_NUMERIC}-101"
|
||||
|
||||
mapfile -t PROCUREMENT_APPROVED < <(create_and_approve_purchase "$EMPLOYEE_TOKEN" "$MANAGER_TOKEN" "procurement erp success" "proc-success")
|
||||
PURCHASE_ID_APPROVED="${PROCUREMENT_APPROVED[0]}"
|
||||
COMMITMENT_DEDUPE="purchase_request:${PURCHASE_ID_APPROVED}:commitment"
|
||||
@@ -1024,7 +1204,7 @@ jq -e '.status == "succeeded"' <<<"$commitment_state_done" >/dev/null
|
||||
commitment_binding="$(query_binding_by_key 'purchase_request_commitment_journal' "$PURCHASE_ID_APPROVED")"
|
||||
jq -e '.externalCode != null' <<<"$commitment_binding" >/dev/null
|
||||
|
||||
ARCHIVE_ID_APPROVED="$(archive_purchase_order "$FINANCE_MGR_TOKEN" "$PURCHASE_ID_APPROVED" 'PO-P19ERP2-001' 'CT-P19ERP2-001' 'proc-success')"
|
||||
ARCHIVE_ID_APPROVED="$(archive_purchase_order "$FINANCE_MGR_TOKEN" "$PURCHASE_ID_APPROVED" "$PROC_SUCCESS_PO" "$PROC_SUCCESS_CONTRACT_1" 'proc-success')"
|
||||
ACCRUAL_DEDUPE="purchase_order_archive:${ARCHIVE_ID_APPROVED}:accrual"
|
||||
SUMMARY_DEDUPE="purchase_order_archive:${ARCHIVE_ID_APPROVED}:summary"
|
||||
accrual_state="$(query_outbox_by_key 'erp.journal.export' "$ACCRUAL_DEDUPE")"
|
||||
@@ -1044,14 +1224,14 @@ jq -e '.externalCode != null' <<<"$po_binding" >/dev/null
|
||||
jq -e '.externalCode != null' <<<"$contract_binding" >/dev/null
|
||||
|
||||
echo "[10/13] Archive update only resets summary"
|
||||
ARCHIVE_UPDATE_STATUS="$(api_json_auth POST "/api/purchase-requests/$PURCHASE_ID_APPROVED/archive-order" "$FINANCE_MGR_TOKEN" "$TMP_DIR/proc-success-archive-update.json" '{"poNo":"PO-P19ERP2-001","contractNo":"CT-P19ERP2-002","archivedFiles":[]}')"
|
||||
ARCHIVE_UPDATE_STATUS="$(api_json_auth POST "/api/purchase-requests/$PURCHASE_ID_APPROVED/archive-order" "$FINANCE_MGR_TOKEN" "$TMP_DIR/proc-success-archive-update.json" "$(jq -nc --arg po "$PROC_SUCCESS_PO" --arg contract "$PROC_SUCCESS_CONTRACT_2" '{poNo:$po, contractNo:$contract, archivedFiles:[]}')")"
|
||||
assert_status "$ARCHIVE_UPDATE_STATUS" "200" "archive update" "$TMP_DIR/proc-success-archive-update.json"
|
||||
accrual_state_after_update="$(query_outbox_by_key 'erp.journal.export' "$ACCRUAL_DEDUPE")"
|
||||
summary_state_after_update="$(query_outbox_by_key 'erp.procurement.export' "$SUMMARY_DEDUPE")"
|
||||
ACCRUAL_OUTBOX_ID="$(jq -er '.id' <<<"$accrual_state_done")"
|
||||
SUMMARY_OUTBOX_ID="$(jq -er '.id' <<<"$summary_state_done")"
|
||||
jq -e --arg id "$ACCRUAL_OUTBOX_ID" '.id == $id and .status == "succeeded"' <<<"$accrual_state_after_update" >/dev/null
|
||||
jq -e --arg id "$SUMMARY_OUTBOX_ID" '.id == $id and .status == "pending" and .payload.contractNo == "CT-P19ERP2-002"' <<<"$summary_state_after_update" >/dev/null
|
||||
jq -e --arg id "$SUMMARY_OUTBOX_ID" --arg contract "$PROC_SUCCESS_CONTRACT_2" '.id == $id and .status == "pending" and .payload.contractNo == $contract' <<<"$summary_state_after_update" >/dev/null
|
||||
run_outbox_once success success
|
||||
contract_binding_updated="$(query_binding_by_key 'purchase_order_contract' "$ARCHIVE_ID_APPROVED")"
|
||||
jq -e '.externalCode != null' <<<"$contract_binding_updated" >/dev/null
|
||||
@@ -1062,7 +1242,7 @@ mapfile -t PROCUREMENT_SKIPPED < <(create_and_approve_purchase "$EMPLOYEE_TOKEN"
|
||||
PURCHASE_ID_SKIPPED="${PROCUREMENT_SKIPPED[0]}"
|
||||
COMMITMENT_DEDUPE_SKIPPED="purchase_request:${PURCHASE_ID_SKIPPED}:commitment"
|
||||
assert_outbox_absent_by_key 'erp.journal.export' "$COMMITMENT_DEDUPE_SKIPPED"
|
||||
ARCHIVE_ID_SKIPPED="$(archive_purchase_order "$FINANCE_MGR_TOKEN" "$PURCHASE_ID_SKIPPED" 'PO-P19ERP2-101' 'CT-P19ERP2-101' 'proc-skipped')"
|
||||
ARCHIVE_ID_SKIPPED="$(archive_purchase_order "$FINANCE_MGR_TOKEN" "$PURCHASE_ID_SKIPPED" "$PROC_SKIPPED_PO" "$PROC_SKIPPED_CONTRACT" 'proc-skipped')"
|
||||
ACCRUAL_DEDUPE_SKIPPED="purchase_order_archive:${ARCHIVE_ID_SKIPPED}:accrual"
|
||||
SUMMARY_DEDUPE_SKIPPED="purchase_order_archive:${ARCHIVE_ID_SKIPPED}:summary"
|
||||
assert_outbox_absent_by_key 'erp.journal.export' "$ACCRUAL_DEDUPE_SKIPPED"
|
||||
@@ -1077,7 +1257,7 @@ jq -e '.status == "succeeded"' <<<"$(query_outbox_by_key 'erp.journal.export' "$
|
||||
jq -e '.status == "succeeded"' <<<"$(query_outbox_by_key 'erp.procurement.export' "$SUMMARY_DEDUPE_SKIPPED")" >/dev/null
|
||||
|
||||
echo "[12/13] Archive summary dead-letter can requeue same outbox"
|
||||
PROC_SUMMARY_FAIL_STATUS="$(api_json_auth POST "/api/purchase-requests/$PURCHASE_ID_APPROVED/archive-order" "$FINANCE_MGR_TOKEN" "$TMP_DIR/proc-summary-fail-archive.json" '{"poNo":"PO-P19ERP2-001","contractNo":"CT-P19ERP2-003","archivedFiles":[]}')"
|
||||
PROC_SUMMARY_FAIL_STATUS="$(api_json_auth POST "/api/purchase-requests/$PURCHASE_ID_APPROVED/archive-order" "$FINANCE_MGR_TOKEN" "$TMP_DIR/proc-summary-fail-archive.json" "$(jq -nc --arg po "$PROC_SUCCESS_PO" --arg contract "$PROC_SUCCESS_CONTRACT_3" '{poNo:$po, contractNo:$contract, archivedFiles:[]}')")"
|
||||
assert_status "$PROC_SUMMARY_FAIL_STATUS" "200" "archive update for summary failure" "$TMP_DIR/proc-summary-fail-archive.json"
|
||||
run_outbox_once success client
|
||||
summary_dead_state="$(query_outbox_by_key 'erp.procurement.export' "$SUMMARY_DEDUPE")"
|
||||
@@ -1090,7 +1270,79 @@ run_outbox_once success success
|
||||
summary_requeue_state="$(query_outbox_by_key 'erp.procurement.export' "$SUMMARY_DEDUPE")"
|
||||
jq -e --arg id "$SUMMARY_DEAD_ID" '.id == $id and .status == "succeeded"' <<<"$summary_requeue_state" >/dev/null
|
||||
|
||||
echo "[13/13] ERP env gate off blocks sync"
|
||||
echo "[13/18] Asset ledger summary auto export + binding"
|
||||
mapfile -t ASSET_FIXTURE_IDS < <(resolve_asset_fixture_ids)
|
||||
ASSET_ASSIGNEE_ID="${ASSET_FIXTURE_IDS[0]}"
|
||||
ASSET_BASE_LEGAL_ENTITY_ID="${ASSET_FIXTURE_IDS[1]}"
|
||||
ASSET_BASE_COST_CENTER_ID="${ASSET_FIXTURE_IDS[2]}"
|
||||
ASSET_SYNCED_LEGAL_ENTITY_ID="${ASSET_FIXTURE_IDS[3]}"
|
||||
ASSET_SYNCED_COST_CENTER_ID="${ASSET_FIXTURE_IDS[4]}"
|
||||
ASSET_CATEGORY_ID="$(create_asset_category "$FINANCE_MGR_TOKEN" 'asset-success')"
|
||||
ASSET_ID_APPROVED="$(create_asset_card "$FINANCE_MGR_TOKEN" "$ASSET_CATEGORY_ID" "$ASSET_BASE_LEGAL_ENTITY_ID" "$ASSET_BASE_COST_CENTER_ID" "$ASSET_ASSIGNEE_ID" 'asset-success')"
|
||||
ASSET_DEDUPE="asset_card:${ASSET_ID_APPROVED}:ledger_summary"
|
||||
asset_state_created="$(query_outbox_by_key 'erp.asset.export' "$ASSET_DEDUPE")"
|
||||
jq -e '.status == "pending" and .payload.exportType == "asset_ledger_summary" and .payload.changeType == "created"' <<<"$asset_state_created" >/dev/null
|
||||
run_outbox_once success success success
|
||||
asset_state_done="$(query_outbox_by_key 'erp.asset.export' "$ASSET_DEDUPE")"
|
||||
jq -e '.status == "succeeded"' <<<"$asset_state_done" >/dev/null
|
||||
asset_binding="$(query_binding_by_key 'asset_erp_card' "$ASSET_ID_APPROVED")"
|
||||
jq -e '.externalCode != null and .lastSyncedAt != null' <<<"$asset_binding" >/dev/null
|
||||
ASSET_OUTBOX_ID="$(jq -er '.id' <<<"$asset_state_done")"
|
||||
ASSET_TRACK_EMPLOYEE_CODE="$(api_json_auth GET "/api/integrations/erp/assets/$ASSET_ID_APPROVED/track" "$EMPLOYEE_TOKEN" "$TMP_DIR/asset-track-employee.json")"
|
||||
assert_status "$ASSET_TRACK_EMPLOYEE_CODE" "200" "employee asset erp track" "$TMP_DIR/asset-track-employee.json"
|
||||
jq -e '.data.stage.deliveryStatus == "succeeded" and .data.stage.binding.externalCode == null and .data.permissions.canRetry == false' "$TMP_DIR/asset-track-employee.json" >/dev/null
|
||||
ASSET_REQUEUE_FORBIDDEN_CODE="$(api_json_auth POST "/api/integrations/erp/assets/$ASSET_ID_APPROVED/requeue?runNow=0" "$EMPLOYEE_TOKEN" "$TMP_DIR/asset-requeue-forbidden.json" '{}')"
|
||||
assert_status "$ASSET_REQUEUE_FORBIDDEN_CODE" "403" "asset requeue forbidden" "$TMP_DIR/asset-requeue-forbidden.json"
|
||||
|
||||
echo "[14/18] Non-financial patch does not reset asset outbox"
|
||||
ASSET_PATCH_NON_FIN_CODE="$(api_json_auth PATCH "/api/assets/$ASSET_ID_APPROVED" "$FINANCE_MGR_TOKEN" "$TMP_DIR/asset-non-financial.json" '{"name":"资产ERP-asset-success-v2","location":"上海办公室-B区"}')"
|
||||
assert_status "$ASSET_PATCH_NON_FIN_CODE" "200" "asset non financial patch" "$TMP_DIR/asset-non-financial.json"
|
||||
asset_state_after_non_fin="$(query_outbox_by_key 'erp.asset.export' "$ASSET_DEDUPE")"
|
||||
jq -e --arg id "$ASSET_OUTBOX_ID" '.id == $id and .status == "succeeded"' <<<"$asset_state_after_non_fin" >/dev/null
|
||||
|
||||
echo "[15/18] Financial patch resets same asset outbox"
|
||||
ASSET_PATCH_FIN_CODE="$(api_json_auth PATCH "/api/assets/$ASSET_ID_APPROVED" "$FINANCE_MGR_TOKEN" "$TMP_DIR/asset-financial.json" "$(jq -nc --arg le "$ASSET_SYNCED_LEGAL_ENTITY_ID" --arg cc "$ASSET_SYNCED_COST_CENTER_ID" '{legalEntityId:$le, costCenterId:$cc}')")"
|
||||
assert_status "$ASSET_PATCH_FIN_CODE" "200" "asset financial patch" "$TMP_DIR/asset-financial.json"
|
||||
asset_state_after_fin="$(query_outbox_by_key 'erp.asset.export' "$ASSET_DEDUPE")"
|
||||
jq -e --arg id "$ASSET_OUTBOX_ID" --arg le "$ASSET_SYNCED_LEGAL_ENTITY_ID" --arg cc "$ASSET_SYNCED_COST_CENTER_ID" '.id == $id and .status == "pending" and .payload.changeType == "financial_dimensions_changed" and .payload.legalEntity.id == $le and .payload.costCenter.id == $cc' <<<"$asset_state_after_fin" >/dev/null
|
||||
run_outbox_once success success success
|
||||
asset_state_after_fin_done="$(query_outbox_by_key 'erp.asset.export' "$ASSET_DEDUPE")"
|
||||
jq -e '.status == "succeeded"' <<<"$asset_state_after_fin_done" >/dev/null
|
||||
|
||||
echo "[16/18] Retired asset dead-letter can requeue same outbox"
|
||||
ASSET_PATCH_RETIRED_CODE="$(api_json_auth PATCH "/api/assets/$ASSET_ID_APPROVED" "$FINANCE_MGR_TOKEN" "$TMP_DIR/asset-retired.json" '{"status":"retired","retiredAt":"2026-03-09"}')"
|
||||
assert_status "$ASSET_PATCH_RETIRED_CODE" "200" "asset retired patch" "$TMP_DIR/asset-retired.json"
|
||||
asset_state_retired_pending="$(query_outbox_by_key 'erp.asset.export' "$ASSET_DEDUPE")"
|
||||
jq -e --arg id "$ASSET_OUTBOX_ID" '.id == $id and .status == "pending" and .payload.changeType == "retired"' <<<"$asset_state_retired_pending" >/dev/null
|
||||
run_outbox_once success success client
|
||||
asset_state_dead="$(query_outbox_by_key 'erp.asset.export' "$ASSET_DEDUPE")"
|
||||
jq -e '.status == "dead_letter" and .attempts == 1 and (.lastError | tostring | test("erp_asset_http_4xx|422"))' <<<"$asset_state_dead" >/dev/null
|
||||
ASSET_DEAD_OUTBOX_ID="$(jq -er '.id' <<<"$asset_state_dead")"
|
||||
ASSET_REQUEUE_CODE="$(api_json_auth POST "/api/integrations/erp/assets/$ASSET_ID_APPROVED/requeue?runNow=0" "$FINANCE_MGR_TOKEN" "$TMP_DIR/asset-requeue.json" '{}')"
|
||||
assert_status "$ASSET_REQUEUE_CODE" "200" "asset requeue" "$TMP_DIR/asset-requeue.json"
|
||||
jq -e --arg id "$ASSET_DEAD_OUTBOX_ID" '.data.outboxId == $id and .data.requeued == true' "$TMP_DIR/asset-requeue.json" >/dev/null
|
||||
run_outbox_once success success success
|
||||
asset_state_requeued_done="$(query_outbox_by_key 'erp.asset.export' "$ASSET_DEDUPE")"
|
||||
jq -e --arg id "$ASSET_DEAD_OUTBOX_ID" '.id == $id and .status == "succeeded"' <<<"$asset_state_requeued_done" >/dev/null
|
||||
|
||||
echo "[17/18] Force-off skips asset auto export and manual requeue backfills historical资产"
|
||||
set_erp_flag "force_off"
|
||||
ASSET_CATEGORY_SKIPPED_ID="$(create_asset_category "$FINANCE_MGR_TOKEN" 'asset-skipped')"
|
||||
ASSET_ID_SKIPPED="$(create_asset_card "$FINANCE_MGR_TOKEN" "$ASSET_CATEGORY_SKIPPED_ID" "$ASSET_BASE_LEGAL_ENTITY_ID" "$ASSET_BASE_COST_CENTER_ID" "$ASSET_ASSIGNEE_ID" 'asset-skipped')"
|
||||
ASSET_DEDUPE_SKIPPED="asset_card:${ASSET_ID_SKIPPED}:ledger_summary"
|
||||
assert_outbox_absent_by_key 'erp.asset.export' "$ASSET_DEDUPE_SKIPPED"
|
||||
ASSET_PATCH_SKIPPED_CODE="$(api_json_auth PATCH "/api/assets/$ASSET_ID_SKIPPED" "$FINANCE_MGR_TOKEN" "$TMP_DIR/asset-skipped-patch.json" "$(jq -nc --arg le "$ASSET_SYNCED_LEGAL_ENTITY_ID" --arg cc "$ASSET_SYNCED_COST_CENTER_ID" '{legalEntityId:$le, costCenterId:$cc}')")"
|
||||
assert_status "$ASSET_PATCH_SKIPPED_CODE" "200" "asset skipped patch" "$TMP_DIR/asset-skipped-patch.json"
|
||||
assert_outbox_absent_by_key 'erp.asset.export' "$ASSET_DEDUPE_SKIPPED"
|
||||
set_erp_flag "force_on"
|
||||
ASSET_BACKFILL_CODE="$(api_json_auth POST "/api/integrations/erp/assets/$ASSET_ID_SKIPPED/requeue?runNow=0" "$FINANCE_MGR_TOKEN" "$TMP_DIR/asset-backfill.json" '{}')"
|
||||
assert_status "$ASSET_BACKFILL_CODE" "200" "asset backfill" "$TMP_DIR/asset-backfill.json"
|
||||
jq -e '.data.duplicated == false and .data.status == "pending"' "$TMP_DIR/asset-backfill.json" >/dev/null
|
||||
run_outbox_once success success success
|
||||
asset_backfill_state="$(query_outbox_by_key 'erp.asset.export' "$ASSET_DEDUPE_SKIPPED")"
|
||||
jq -e '.status == "succeeded"' <<<"$asset_backfill_state" >/dev/null
|
||||
|
||||
echo "[18/18] ERP env gate off blocks sync"
|
||||
if kill -0 "$APP_SERVER_PID" >/dev/null 2>&1; then
|
||||
kill "$APP_SERVER_PID" >/dev/null 2>&1 || true
|
||||
wait "$APP_SERVER_PID" >/dev/null 2>&1 || true
|
||||
@@ -1105,8 +1357,10 @@ ERP_SYNC_PROVIDER=http \
|
||||
ERP_MASTER_DATA_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/master-data/pull?mode=success" \
|
||||
ERP_JOURNAL_EXPORT_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/journals/export?mode=success" \
|
||||
ERP_PROCUREMENT_EXPORT_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/procurement/export?mode=success" \
|
||||
ERP_ASSET_EXPORT_ENDPOINT="http://127.0.0.1:$MOCK_ERP_PORT/erp/assets/export?mode=success" \
|
||||
ERP_SYNC_TIMEOUT_MS="$ERP_SYNC_TIMEOUT_MS" \
|
||||
ERP_PROCUREMENT_TIMEOUT_MS="$ERP_SYNC_TIMEOUT_MS" \
|
||||
ERP_ASSET_TIMEOUT_MS="$ERP_SYNC_TIMEOUT_MS" \
|
||||
ERP_MASTER_DATA_TIMEOUT_MS="$ERP_SYNC_TIMEOUT_MS" \
|
||||
ERP_SYNC_AUTH_TOKEN="" \
|
||||
node src/index.js >"$TMP_DIR/app-gate-off.log" 2>&1 &
|
||||
|
||||
在新工单中引用
屏蔽一个用户