借款出借人线上确认借款并以站内通知直达(09-17-company-loan-principal-repayment)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

- 新增出借人确认声明:财务发起,出借人本人查看原件后确认或退回;快照与决定不可改删,金额、日期、原件或身份变化后整组失效
- 余额确认接受纸质协议或有效线上声明二选一,代账余额核对材料仍必需;声明与公司代表授权分离
- 普通员工无常驻入口,财务发起后仅通知对应出借人并直达确认页
- 新增迁移 20261010010000_loan_lender_acknowledgements(仅新增三表)
- 测试:单测、隔离 HTTP/浏览器、完整借款回归;独立只读复核无阻断;同步发布记录与操作手册

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
2026-10-10 15:22:12 +08:00
共同撰写人 Claude Opus 5.5
父节点 2063ff1e93
当前提交 4a5cd8f2b3
共修改 34 个文件,包含 1610 行新增和 41 行删除
@@ -1145,6 +1145,12 @@ await canReadReimbursementBusinessFact(prisma, reimbursement, employee.id);
- `09-17-company-loan-principal-repayment` adds the explicit `loan_repayment` payment source: one retained repayment/batch/item, multiple allocations to confirmed same-company/lender/CNY loans. Preserve original principal and confirmed opening evidence; Decimal reservations and settlements must not exceed opening outstanding principal. SQL checks/deferred triggers complement locked application validation.
- Loan management requires an explicit finance/finance_mgr role plus loan.manage. Independent review requires active manager/CEO with review permission and excludes maker, submitter and lender OA identity; admin wildcard cannot replace these identities. External lenders require explicit verified no-OA identity. Manual account changes revoke identity confirmation and invalidate old unpaid snapshots; explicit employee-source refresh renews confirmation under the checks below.
- `LoanLenderAcknowledgement` is the lender's declaration, separate from company-representative authority. Finance proposes 1–50 unconfirmed loans for one verified employee lender/company, freezing principal, opening balance/date, incoming originals and identity versions. `loan.acknowledge_own` permits only the frozen employee's own records/originals/decision; manager or administrator status cannot substitute for that identity. Keep the finance maker distinct, originals checked on confirmation, strict request-key/hash binding and audit atomicity.
- Header, scope snapshots and the single confirm/return decision are immutable. SQL triggers invalidate the whole group permanently after any member's material terms, incoming original or lender association/version changes; restoring source values cannot reactivate it. Additional accounting evidence, balance confirmation and ordinary loan version increases do not invalidate. Returned/invalid statements can be reissued; overlapping current pending/confirmed scopes cannot. After balance confirmation, reissue is allowed only with invalidated declaration history for that loan; never rewrite the original balance confirmation, loan version or terms. Service, SQL scope guard and `lenderAcknowledgementRenewable` UI projection must agree. Purely paper-confirmed loans do not gain first-time online declaration scope.
- `assertEvidenceComplete` accepts paper agreement OR a valid confirmed declaration. Incoming originals, accounting reconciliation and required historical-repayment adjustment evidence stay mandatory at balance and repayment consumers. Balance confirmation must lock lender then loan, reread the lender association and evidence, and refuse after a concurrent invalidation; locking only that loan misses sibling-original and identity changes. Declaration writes lock lender → sorted loans → declaration → employee (`FOR NO KEY UPDATE`). Preserve existing payment-batch lock order.
- Declaration-scoped downloads expose only frozen incoming originals after current source and physical digest checks, with purpose, audit and no-store; never expose accounting attachments or other loans via this scope. JSON POSTs through `apiRequestBlob` must explicitly set `Content-Type: application/json`. UI requests are actor-scoped and cancellable; a late 401 cannot replay an old decision under a new account. Ordinary employees have no permanent company-loan entry in workspace navigation, NavLinks fallback or MobileMePage; keep own-only route access independent of navigation visibility. Finance and company representatives retain their existing entries.
- Proposals create one `loan.lender_acknowledgement_requested` in-app notification for the frozen lender employee, in the same transaction as the declaration and audit. Idempotent retries do not create another notification, and a notification write failure rolls back all three. Keep copy free of lender names, amounts, bank details and originals; do not add this type to IM support or external delivery/outbox. Desktop/mobile notification targets use `loan_lender_acknowledgement` and the declaration ID to open details via `lenderAcknowledgementId`, independent of the list page. The URL ID is the single source of current selection and overrides proposal mode; ID changes clear drafts/confirmation state and cancel old requests. Existing ownership and invalidation checks still govern historical links.
- Verify declaration contracts with `RUN_ISOLATED_LOAN_ACK_ONLY=1` on the isolated loan runner and `RUN_ISOLATED_LOAN_BROWSER=1` for desktop/mobile originals, confirm/return, accounting requirement and actor-switch cancellation. Retain full-loan regression. Migration `20261010010000_loan_lender_acknowledgements` adds history without backfilling acceptance; preserve it on rollback. An older application cannot consume online-only agreement evidence, so post-use rollback requires compatibility planning.
- Employee-source preview/link/refresh additionally require `employee.bank.read` or `employee.bank.manage`. `loanLenderEmployee` reads the active employee, supported bank and encrypted account on the server; the strict write body carries employee/company IDs, source version and an explicit confirmation, never client-supplied account/name replacements. A source version binds actual source fields, ciphertext and the chosen debtor, not only `updated_at`. Missing, unreadable, stale or unsupported sources reject without a partial lender/audit write; previews expose masks only.
- Same-company employee association includes archived/history rows in duplicate checks, including the old identity-confirmation entry. Reuse a unique matching lender; reject ambiguous links and require explicit refresh for different data. Existing-lender actions lock lender before sorted employees. New creation never locks an existing lender after employees: finish and reacquire in the correct order if a link appeared. Use employee `FOR NO KEY UPDATE` to serialize association/source edits while allowing audit/actor FK `KEY SHARE`; `FOR UPDATE` can deadlock against another employee's refresh/audit. Persist and serialize each actor's request-key binding for both creation and reuse, with audit in the same transaction.
- Explicit employee refresh rejects reserved repayments, checks source/identity/account versions and advances both lender versions with renewed finance identity confirmation. This invalidates old company-representative grants without changing principal or historical payment snapshots. Employee-linked lenders cannot use the manual account PATCH; employee-master edits alone never rewrite the loan copy. Verify the actual audit-FK interleaving, duplicate/legacy races, refresh versus repayment creation, audit rollback and reverse mutations using `RUN_ISOLATED_LOAN_EMPLOYEE_ONLY=1` and optional `RUN_ISOLATED_LOAN_EMPLOYEE_MUTATIONS=1` on the isolated loan runner; retain the original full-loan regression.