5.7 KiB
EDU-021 — Import legacy tenant payment accounts into native Pay
- Status: done — bounded single-account import, audit, and native Pay UI entry implemented and verified
- Type: legacy data bridge / payment security / tenant isolation
- Phase: 4 / tenant operations
- Blockers: EDU-020, native Pay App/Channel runtime, PostgreSQL Flyway
Problem
The legacy tenant_payment_accounts and app_private.tenant_secrets records cannot be copied directly into native Pay. Provider aliases, collection modes, channel variants, callback ownership, credential shapes, and status values are not one-to-one. Guessing any of them can route money or callbacks to the wrong party.
The migration also needs durable evidence without creating an Education payment shadow model or persisting a second plaintext credential copy.
Delivered contract
- The bridge is Pay-owned and creates native
pay_appandpay_channelrows throughPayAppServiceandPayChannelService; Education owns neither a payment account nor a credential table. POST /pay/legacy-account-import/importimports exactly one explicitly reviewed manifest. It requires bothpay:app:createandpay:channel:create.GET /pay/legacy-account-import/pageexposes tenant-filtered audit history and requires both Pay App and Channel query permissions.- The existing
pay/app/indexVben page adds a 迁移旧支付账号 action and JSON manifest modal. The button uses explicit AND permission visibility, matching the controller. - Import request-body logging is disabled for database access logs, non-production request logs, and unexpected-error logs so the manifest does not become a plaintext logging side channel.
- Only
tenant_collectis accepted.platform_collectandservice_providerfail closed because their settlement and merchant ownership semantics are not equivalent. - Historical WeChat and Alipay aliases normalize to
wechat_payoralipay. Non-equivalent providers such as XPay/Xunhu fail closed. - Operators must explicitly select a native channel such as
wx_lite,wx_pub, or an Alipay variant. Provider family and channel family must match; the importer never guesses a WeChat client type. - WeChat V3 and Alipay public-key configurations map into native Pay configuration objects. Multiple rotating WeChat platform keys require an explicit choice. Alipay accepts only the native production or sandbox official gateway.
- Old provider callbacks are not reused. The manifest must provide new business order/refund callbacks and may provide a transfer callback.
activemaps to enabled.disabledandpendingmap to disabled with an audit note.- Within the current target tenant,
sourceAccountIdis the idempotency key. A replay with the same source SHA-256 returns the existing mapping; a different checksum is rejected rather than overwriting it.
Audit and database contract
V4330 creates tenant-scoped pay_legacy_account_import with source identifiers/checksum, normalized provider/config digest, target App/Channel IDs, mapping notes, operator, and timestamp. It deliberately has no config_public, secret_json, raw config, or secret-value column.
Composite foreign keys (tenant_id,target_app_id) and (tenant_id,target_channel_id) prevent an audit row from pointing across tenants. A target tenant may import the same legacy UUID independently, while duplicate active source IDs inside one tenant are rejected. An existing global audit table without tenant_id causes migration failure and requires explicit disposition.
Credentials still enter native pay_channel.config using Pay's existing configuration storage. EDU-021 prevents an extra audit copy; it does not introduce generic encryption or key rotation for native Pay credentials.
Verification
- Nine focused importer tests pass for WeChat/Alipay mapping, aliases, disabled-state mapping, replay, checksum conflict, unsupported modes/providers, channel mismatch, unsafe endpoints/rotating keys, and audit redaction. A controller contract verifies dual write permission and request-body logging suppression.
- The combined Pay selection passes 29 tests, including the prior tenant App/Channel contracts.
- All 45 PostgreSQL Flyway integration tests pass through V4380. V4330 coverage verifies tenant-independent legacy UUID reuse, no raw credential columns, cross-tenant composite-FK rejection, mode constraints, migration history, and fail-closed global-table adoption.
- The Vben
@vben/web-antdtypecheck passes with the import API, modal, and explicit dual-permission button.
Explicitly open
- Audit history currently has a backend/Vben API contract but no dedicated history table in the account-import modal. EDU-023 provides its own recent transaction-import history table on the native order page.
- A controlled export job from the legacy database and operator runbook are still required before production bulk migration. The UI template contains placeholders and must never be submitted unchanged.
- Concurrent first imports of the same source account rely on the database unique constraint and transaction rollback; a friendly concurrent-replay response is not claimed.
- Platform/service-provider settlement, XPay/Xunhu replacement, generic credential encryption/rotation, and tenant PNVS require separate decisions.
- EDU-022 activates tenant-scoped native Pay order, refund, and notification ledgers; EDU-023 adds bounded terminal legacy transaction import; EDU-024 activates empty native Transfer/Wallet ledgers. Production bulk tooling and reviewed opening-balance migration remain open.
- EDU-025 delivers native Mall Product activation. Explicit legacy product import, Promotion/Trade, coupon import/redemption, purchase fulfillment, refunds-to-entitlement revocation, and reconciliation remain separate commercialization work.