15 KiB
15 KiB
Enterprise WeChat Boundary
Ownership
Use an Enterprise-WeChat-first, OA-complements model for this single-company, 5–10-person internal system:
- Enterprise WeChat owns mature commodity capabilities: workbench entry, OAuth/SSO, directory and basic organization, application messages, HR/attendance actions and low-risk, simple-field, mobile-first approvals.
- General administration and simple procurement with no downstream financial handling, sensitive attachment or cross-business ledger may use Enterprise WeChat for submission and approval after a dedicated task verifies the real template and approval chain. OA retains only a minimum reference when needed.
- OA owns the complete reimbursement journey: request, invoice upload/PDF/OCR and duplicate detection, approval, finance review, payment status and receipt. Enterprise WeChat provides SSO/workbench entry, dehydrated notifications and exact deeplinks into the relevant OA page; users must not re-enter or chase the same reimbursement across both systems.
- OA also owns financial ledgers, payment/AP and maker-checker, contract bodies, sensitive attachments, cross-business relationships and long-term audit.
This boundary is a selection rule, not evidence that any existing procurement or administration workflow has already migrated.
Rules
- Enterprise WeChat API checks that need trusted IP must run from the deployment server, not local.
- Do not write Enterprise WeChat, create templates, create approvals, expand real users, or enable live trials without explicit approval and a Trellis task.
- Approval-rule configuration cannot be fully read through official APIs; keep manual backend/real-device gates where specified.
- Messages to Enterprise WeChat must be dehydrated and link back to OA; do not send sensitive attachments or business documents into native WeCom approvals.
- Prefer a WeCom-native mobile form over duplicating it in OA when the process is simple and low-risk. Do not build full payload mirrors, bidirectional writeback, duplicate native approval UIs or generic approval-rule operations unless a later task proves a concrete business and audit need.
- For every WeCom approval promoted to formal use, manually verify the backend rule, applicant-visible mobile submission page and actual post-submission approval chain; API template metadata alone is insufficient.
Scenario: Compensatory Leave Is A WeCom Leave Category
1. Scope / Trigger
- Trigger: adding, displaying, validating or operating compensatory leave
(
调休) in the WeCom-owned HR/attendance flow. - Purpose: support the required leave category without creating a fifth approval integration, a parallel OA submission journey or an unneeded balance engine.
2. Signatures
- WeCom approval target remains
leave; the existing target set remainsleave,missed_punch,outing,overtime. - WeCom template/form category label:
调休under the leave template. - OA read-only archive fields: normalized leave category, interval, status and minimum approval summary. No new ordinary OA submit/approve API is implied.
3. Contracts
- WeCom owns employee submission, approval and normal mobile history. OA must
distinguish
调休from other leave categories in read-only history but must not expose a parallel ordinary submission or approval flow. - The initial internal-team scope does not build an OA compensatory-leave balance, automatic overtime-to-leave conversion, carry-over engine or payroll calculation. If WeCom native balance management is enabled, it is the source of truth; otherwise HR uses a controlled manual rule until a later task proves automation is worth maintaining.
- Template metadata is insufficient acceptance evidence. Formal use requires a backend check, applicant-visible real-device form and actual submitted approval chain with no self-approval.
4. Validation & Error Matrix
- WeCom backend lacks
调休-> remain not ready; do not emulate it with an OA ordinary form. - Real-device form does not show
调休->PAUSED_MANUAL_CONFIRMATION. - Applicant can select/change the approver or becomes the approver -> blocker; do not enable real-team use.
- OA archive cannot distinguish the leave category -> keep the data read-only but do not claim compensatory-leave acceptance.
- WeCom native balance is absent -> use the approved manual HR rule; do not silently create an OA balance ledger.
5. Good/Base/Bad Cases
- Good: a test user selects
调休in WeCom, a different real approver handles it, and OA read-only history displays调休, interval and final status. - Base: WeCom manages the request while HR maintains the small-team balance by a controlled manual rule; OA stores no balance.
- Bad: introduce a
compensatory_leaveapproval target and duplicate submit, balance and approval state across OA and WeCom.
6. Tests Required
- Unit/fixture coverage proves a WeCom
leavedetail containing调休remains targetleaveand preserves the category in the minimum archive/display path. - Mocked frontend coverage proves ordinary employees see read-only history and no OA ordinary compensatory-leave submit action.
- Formal-use evidence must include the backend option, real-device form and actual no-self-approval chain; live writes require a separately approved task.
7. Wrong vs Correct
Wrong
调休 -> new compensatory_leave target -> OA balance -> OA approval -> WeCom sync
Correct
WeCom leave(category=调休) -> WeCom approval -> OA minimum read-only history
HR / Attendance UI Contracts
请假记录is the PC leave entry label and page title. Ordinary employees must see records, balances and a WeCom-owned explanation, not an OA leave creation form.- OA leave creation, edit, submit and withdraw controls are emergency-only UI.
They require
leave.manageand must be labeled withOA 应急/应急草稿/提交应急审批; do not expose them based only onleave.create. - Mobile leave defaults to records for ordinary employees. Hide the form tab and
row actions unless the user has
leave.manage; keep the first card explicit:企业微信主办and只读. - Attendance correction forms are emergency-only UI. They require
attendance.manageand useOA 应急修正/提交应急修正. Ordinary employees see a WeCom handling explanation for missed punch, outing and overtime. - WeCom approval archive pages are read-only history and reconciliation. They may show status/action history labels from WeCom data, but must not offer OA approve/reject/return/transfer actions.
Tests Required
- Mocked frontend Playwright coverage should assert that ordinary employees do not see OA leave/correction create actions, while admin/HR roles retain explicitly labeled emergency entry points.
- Full-stack Playwright coverage that writes local data still requires a running local backend/API environment; a Vite-only run is not a valid failure signal for those real-chain tests.
Scenario: One-Time Private-Info OAuth Onboarding
1. Scope / Trigger
- Trigger: obtaining phone/email for an app-visible member or allowing an unbound active member to create the minimum OA account.
- This is a one-time onboarding path, not the ordinary WeCom login path and not a replacement for administrator-assigned finance, CEO or admin roles.
2. Signatures
- Entry:
GET /api/auth/wecom/login?mode=onboard&returnTo=<internal-path>. - Ordinary entry: the same route with
mode=login|bind. - Provider chain:
snsapi_privateinfo->/cgi-bin/auth/getuserinfo->/cgi-bin/auth/getuserdetailwith{ "user_ticket": "<short-lived>" }. - Config:
WECOM_OAUTH_ONBOARDING_ENABLED=falseby default. - Directory chain:
WECOM_CONTACT_SECRETonly for/user/list_id;WECOM_AGENT_SECRETfor/user/get,/department/listand/hr/get_staff_info.
3. Contracts
login|bindalways keepssnsapi_base; only explicitonboardusessnsapi_privateinfo.- The member must be visible to the app, active, return the same userid from both identity calls, and authorize a valid personal email or business email. Phone may be absent; when returned it must be normalized and checked for uniqueness. A blank/invalid personal email may fall back to a valid business email.
user_ticket, OAuth code, access token and raw provider responses exist only in request memory. Do not persist them or include them in Cookie, audit, response details or logs; provider/network failures use fixed safe codes.- Resolve userid, phone and email conflicts before writing. A member already bound to another employee or an employee bound to another member is rejected.
- New accounts are created transactionally with employee, exactly the
employeerole, identity binding and dehydrated audit. Existing active accounts may complete normalized profile data and binding but keep all roles. - The feature flag controls the entry and callback provisioning path. Enabling it and changing real app permissions are production configuration operations requiring separate approval.
4. Validation & Error Matrix
- Flag disabled -> entry is
404; no private scope or account write. - Missing
user_ticket-> safeconsent_requiredredirect; no write. - Provider/network failure ->
wecom_api_failedor safe auth error; no raw payload, token or ticket in output/logs. - Userid mismatch ->
identity_mismatch; inactive member ->member_inactive. - Both emails invalid or unresolved department ->
missing_required_field. - Phone absent -> continue with
NULL; phone present but conflicting ->employee_conflict. - Multiple employees, unique-field race or cross-binding ->
employee_conflict. - Department binding points to missing/archived data ->
department_invalid; multiple same-name OA departments ->department_ambiguous.
5. Good/Base/Bad Cases
- Good: an unbound active member explicitly consents, receives an employee-only account and authenticated session, then an administrator separately assigns any business role with existing audit/session-revocation semantics.
- Base: a bound active employee repeats onboarding to complete normalized profile data; no role is added or removed.
- Bad: change ordinary login to
snsapi_privateinfo, use batch/user/getabsence as proof that email is unavailable, or auto-grantfinance/ceo.
6. Tests Required
- Route tests for default-off, scope separation, missing ticket, safe provider failure, successful session and employee-only provisioning.
- Service tests for active state, userid match, personal-to-business email fallback, invalid fields, multiple matches, cross-binding and existing-role preservation.
- Assertions must prove ticket, token, raw payload, phone and email are absent from audit/error serialization; run full backend tests and Prisma validation.
7. Wrong vs Correct
Wrong
ordinary login -> snsapi_privateinfo -> batch sync guesses missing fields
-> auto-grant finance/ceo
Correct
ordinary login -> snsapi_base
one-time onboard -> explicit private consent -> safe unique employee-only write
business role assignment -> separate administrator-audited operation
Scenario: WeCom Directory Export With Optional Phone
1. Scope / Trigger
- Trigger: importing an administrator-exported WeCom directory XLSX, or
changing
Employee.phoneacross database, API, HR sync, OAuth and UI layers. - Purpose: allow a real member to remain temporarily without a phone while preserving stable identity, uniqueness and privacy controls.
2. Signatures
- DB:
Employee.phone String? @unique; migration drops only theNOT NULLconstraint and keeps the unique index. - CLI:
npm run import:wecom-directory-export -- --input=- --dry-run. - Apply:
WECOM_DIRECTORY_EXPORT_APPLY=1 ... --input=- --apply --actor-employee-no=<employee-no>. - User API and frontend employee/user types expose
phone: string | null; create/update accepts an omitted or explicitnullphone.
3. Contracts
- Missing phone is stored as SQL
NULL, never"", a repeated placeholder or a synthesized fake number. MultipleNULLrows are valid; non-null phones remain unique. - WeCom account and valid enterprise email remain required for directory-export onboarding. Account, non-null phone and enterprise email duplicates block the input.
- An empty source phone never clears an existing OA phone. Only an explicitly supplied valid phone participates in update and unique-conflict checks.
- The XLSX is accepted only from stdin and defaults to dry-run. Apply requires
both
--applyandWECOM_DIRECTORY_EXPORT_APPLY=1, plus an active actor. - CLI output contains counts and categories only. The workbook, full local path, names, accounts, phones, emails, departments and raw rows must not enter Git, Trellis artifacts or reports.
- New accounts receive only
employee; finance, CEO and admin roles remain a separate administrator-audited operation.
4. Validation & Error Matrix
- Missing phone cell -> valid profile with
phone=null. - Supplied malformed phone -> incomplete/blocked; do not downgrade it to an absent phone and do not write it.
- Missing/invalid enterprise email, account, name or department -> skipped or
blocked as
missing_required_field. - Duplicate account/non-null phone/enterprise email in the workbook ->
duplicate_values; error output contains counts only. - Existing OA unique-field or binding conflict -> conflict; no automatic merge.
--applywithout confirmation env or active actor -> refuse before sync.- Migration rollback with any
phone IS NULLrow -> stop; do not fill fake values merely to restoreNOT NULL.
5. Good/Base/Bad Cases
- Good: five members have unique phones, one member has no phone, all six have unique accounts and enterprise emails; dry-run plans six records and reports only aggregate counts.
- Base: a later export still omits a member phone; the member's existing OA phone is preserved.
- Bad: write an empty string or shared placeholder, use personal email in place of the required enterprise email for bulk identity, or auto-grant a business role during import.
6. Tests Required
- Prisma validation plus a migration review proving only
DROP NOT NULL. - Unit tests for instruction rows/Chinese headers, five-present/one-missing, malformed phone, missing enterprise email and duplicate account/phone/email.
- Route/service tests for create/update/import
null, non-empty unique checks, OAuth missing phone and empty-source preservation of an existing phone. - Frontend typecheck/build and displays that render a clear empty-phone label.
- Full backend regression, Trellis governance, sensitive-content scan and
git diff --check; a real workbook may be parsed only into aggregate counts and remains Git-ignored.
7. Wrong vs Correct
Wrong
phone: source.phone || "00000000000"
Correct
phone: source.phone || null
// Existing employee updates omit `phone` entirely when the source is empty.