24 KiB
Host, Agency And BD Admin Architecture
本文只定义后台/Admin 侧实现:政策配置、工资周期、工资单、审核、钱包入账、调整单、后台审计和运营干预。App 侧的主播申请、Agency 成员、BD 邀请和用户可见查询见 Host Agency BD App Architecture。
这里的工资统一指发放到主播侧 USD_BALANCE 的业务奖励。提现审核和人工打款仍由 wallet-service 拥有,Admin 只负责把审核通过的工资单入账到工资余额,不直接处理线下打款。
本文不新增独立 host-service 微服务。host、Agency、BD、政策和工资后台能力属于 user-services,代码落在当前仓库的 services/user-service host domain。
Goals
- 后台可以配置 host、agency、bd 三类工资政策,并按区域生效。
- 后台可以选择日结、周结、半月结、月结,不把结算周期写死在代码里。
- 后台可以生成工资周期,查看每个 host/Agency/BD/BD Leader 的明细来源。
- 后台审批后,
user-servicehost domain 以幂等方式调用wallet-service给USD_BALANCE入账。 - 后台可以对迟到事件、人工修正、争议处理创建调整单,不能直接修改已入账工资项。
- 后台可以创建或停用 BD Leader、BD、Agency,也可以关闭 Agency;后台可以查看和新增币商
coin_seller;所有动作必须有审计记录。
Non-Goals
- 不在 Admin 里承载 App 长连接、房间麦位、房间核心状态或 IM 投递。
- 不让
wallet-service理解 host/Agency/BD 政策。钱包只接收已审批的入账请求。 - 不用 Redis 保存政策、工资周期、工资单或审计事实。Redis 只能做短 TTL 查询缓存和互斥锁兜底。
- 不允许后台在不留审计的情况下改关系、改政策、改工资金额。
- 不允许已 posted 的工资单原地改金额;必须创建反向或补差调整项。
Service Boundaries
graph LR
Admin["Admin Console"] --> AdminServer["hyapp-admin-server"]
AdminServer --> User["user-service\nhost domain"]
AdminServer --> Wallet["wallet-service"]
AdminServer --> AdminDB[("hyapp_admin MySQL")]
User --> UserDB[("user MySQL")]
Wallet --> WalletDB[("wallet MySQL")]
RoomOutbox[("room_outbox")] --> MQ["MQ / event bus"]
WalletOutbox[("wallet_outbox")] --> MQ
MQ --> User
User --> HostOutbox[("host_outbox")]
HostOutbox --> Message["message inbox owner"]
HostOutbox --> BI["BI / risk / export"]
| Service | Admin Responsibility |
|---|---|
hyapp-admin-server |
Admin HTTP 入口、后台鉴权、request envelope、后台操作审计;使用独立 hyapp_admin 库 |
user-service |
用户、区域、账号状态查询;host/Agency/BD/coin_seller 政策、周期、工资单、关系管理、统计聚合、幂等结果、领域 outbox;App/客户端不能提交区域,后台关系命令的 region_id 只能来自 hyapp-admin-server 鉴权审计入口 |
room-service |
只产出上麦和礼物事件,不接收后台工资命令 |
wallet-service |
COIN、COIN_SELLER_COIN、USD_BALANCE 入账、提现冻结、提现审核状态和钱包流水 |
Admin Permission Model
后台权限至少拆成四类,避免一个运营账号同时拥有配置政策和发钱能力。
| Permission | Can Do | Cannot Do |
|---|---|---|
host_relation_admin |
创建/停用 BD Leader、BD、Agency,关闭 Agency,查看和新增 coin_seller | 审批工资入账 |
salary_policy_admin |
创建政策草稿、启停政策、配置周期 | 直接发工资 |
recharge_policy_admin |
配置区域充值汇率,例如 80000 金币记 1 美元充值 | 重算历史充值金额 |
salary_reviewer |
查看工资单、驳回、请求重算 | 调用钱包入账 |
salary_approver |
审批工资周期、触发 posting | 修改政策和关系 |
所有后台命令必须由 hyapp-admin-server 写入 hyapp_admin.admin_operation_logs。user-service 只保存 host/Agency/BD 业务事实、command_id 幂等结果和领域事件,不创建也不写后台审计表,避免后台库与用户业务库耦合。
币商后台入口同样遵守这个边界:hyapp-admin-server 负责权限和审计,user-service 保存 coin_seller_profiles 身份事实,wallet-service 保存 COIN_SELLER_COIN 余额和转账流水。后台列表可以聚合展示币商身份、用户基础资料和币商余额,但不能把余额字段落到 admin 库或 user 库。
币商给玩家转金币会形成用户充值记录,充值金额由 wallet-service.wallet_recharge_policies 的区域政策快照决定;后台修改充值政策只能影响新交易,不能重算历史 wallet_recharge_records。
| Permission | Kind | Usage |
|---|---|---|
coin-seller:view |
menu |
币商列表、币商余额展示 |
coin-seller:create |
button |
新增币商身份 |
coin-seller:update |
button |
启用或禁用币商身份 |
后台管理某个 App 的 host、Agency、BD、政策或工资时,必须先在后台上下文中选择 app_code。hyapp_admin 库不按 App 拆库;审计行可以在 detail 中记录 app_code。业务事实仍写入 user/wallet 等业务库,并由对应表的 app_code 隔离。
当前 user_id 是内部稳定主键,后台仍必须在所有 host/Agency/BD 查询、幂等和审计 detail 中带上 app_code。如果某些现有表还使用全局 PRIMARY KEY(user_id)、PRIMARY KEY(command_id) 或 PRIMARY KEY(event_id),这只能说明当前实现暂时要求这些 ID 全局唯一;不能据此省略 app_code,也不能在多 App 写入前声称同一个 command_id 可以跨 App 复用。
admin_operation_logs(
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
request_id VARCHAR(64) NOT NULL DEFAULT '',
user_id BIGINT UNSIGNED NOT NULL,
username VARCHAR(64) NOT NULL,
action VARCHAR(80) NOT NULL,
resource VARCHAR(120),
resource_id VARCHAR(80) NOT NULL DEFAULT '',
status VARCHAR(32) NOT NULL,
detail TEXT,
created_at_ms BIGINT NOT NULL,
INDEX idx_admin_operation_logs_request_id (request_id),
INDEX idx_admin_operation_logs_resource_id (resource_id),
INDEX idx_admin_operation_logs_action (action)
);
Phase 2 关系管理的 detail 至少包含 command_id、目标对象、最终状态和 reason;如后续要做结构化前后快照,应在 hyapp_admin 库新增专用审计表或扩展 admin_operation_logs,不能把审计行写入 user-service 库。
Policy Model
政策必须版本化。工资结算使用周期生成时锁定的 policy snapshot,不能用当前 active policy 重算历史。
salary_policies(
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
policy_id BIGINT NOT NULL PRIMARY KEY,
policy_type VARCHAR(32) NOT NULL,
region_id BIGINT NOT NULL,
name VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL,
version VARCHAR(64) NOT NULL,
effective_from_ms BIGINT NOT NULL,
effective_to_ms BIGINT NULL,
settlement_cadence VARCHAR(32) NOT NULL,
created_by_user_id BIGINT NOT NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
KEY idx_salary_policies_region_type (app_code, region_id, policy_type, status)
);
salary_policy_rules(
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
rule_id BIGINT NOT NULL PRIMARY KEY,
policy_id BIGINT NOT NULL,
level_no INT NOT NULL,
min_mic_minutes BIGINT NOT NULL DEFAULT 0,
min_gift_points BIGINT NOT NULL DEFAULT 0,
min_effective_days INT NOT NULL DEFAULT 0,
min_base_salary_usd_minor BIGINT NOT NULL DEFAULT 0,
fixed_salary_usd_minor BIGINT NOT NULL DEFAULT 0,
commission_bps INT NOT NULL DEFAULT 0,
metadata_json JSON NULL,
UNIQUE KEY uk_salary_policy_level (app_code, policy_id, level_no)
);
| Type | Applies To | Base |
|---|---|---|
host |
active host work stats | mic minutes, gift points, effective days |
agency |
Agency owner extra salary | Agency hosts' host salary sum, count, or agency-specific metrics |
bd |
BD and BD Leader salary | downstream hosts' host salary sum |
BD 的基数只能是 host salary。Agency salary 和 BD salary 不能递归进入 BD 基数,否则工资会复利膨胀且无法审计。
Settlement Cadence
后台可以按区域和政策类型选择 daily、weekly、semi_monthly、monthly。
salary_cycle_configs(
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
config_id BIGINT NOT NULL PRIMARY KEY,
region_id BIGINT NOT NULL,
policy_type VARCHAR(32) NOT NULL,
settlement_cadence VARCHAR(32) NOT NULL,
timezone VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
KEY idx_salary_cycle_config_scope (app_code, region_id, policy_type, status)
);
同一区域、同政策类型只允许一个 active config。MySQL 没有 partial unique index 时,不要用 nullable 或 status 唯一键硬凑;在启用配置时用事务锁住同 scope 的 active rows,再做状态切换。
| Cadence | Rule |
|---|---|
daily |
local date 00:00:00 to next date 00:00:00 |
weekly |
configured week start, default Monday |
semi_monthly |
day 1-15 and day 16-end |
monthly |
calendar month |
计算必须使用配置时区。数据库保存 UTC 毫秒边界,同时保存后台导出需要的展示日期。
Payroll Model
flowchart LR
A["host_daily_stats"] --> B["salary cycle calculation"]
B --> C["host salary items"]
C --> D["agency salary items"]
C --> E["BD or BD Leader salary items"]
C --> F["review"]
D --> F
E --> F
F --> G["approve"]
G --> H["wallet CreditSalaryBalance"]
H --> I["USD_BALANCE"]
salary_cycles(
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
cycle_id BIGINT NOT NULL PRIMARY KEY,
region_id BIGINT NOT NULL,
cycle_type VARCHAR(32) NOT NULL,
period_start_ms BIGINT NOT NULL,
period_end_ms BIGINT NOT NULL,
display_period VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
policy_snapshot_json JSON NOT NULL,
source_watermark_ms BIGINT NOT NULL,
created_by_user_id BIGINT NOT NULL,
approved_by_user_id BIGINT NULL,
posted_at_ms BIGINT NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
UNIQUE KEY uk_salary_cycle (app_code, region_id, cycle_type, period_start_ms, period_end_ms)
);
salary_items(
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
salary_item_id BIGINT NOT NULL PRIMARY KEY,
cycle_id BIGINT NOT NULL,
receiver_user_id BIGINT NOT NULL,
receiver_role VARCHAR(32) NOT NULL,
salary_component VARCHAR(32) NOT NULL,
region_id BIGINT NOT NULL,
relation_scope_key VARCHAR(160) NOT NULL,
agency_id BIGINT NULL,
bd_user_id BIGINT NULL,
bd_leader_user_id BIGINT NULL,
policy_id BIGINT NOT NULL,
policy_version VARCHAR(64) NOT NULL,
hit_level INT NOT NULL,
base_host_salary_usd_minor BIGINT NOT NULL DEFAULT 0,
mic_minutes BIGINT NOT NULL DEFAULT 0,
gift_points BIGINT NOT NULL DEFAULT 0,
effective_days INT NOT NULL DEFAULT 0,
amount_usd_minor BIGINT NOT NULL,
status VARCHAR(32) NOT NULL,
wallet_transaction_id VARCHAR(96) NOT NULL DEFAULT '',
metadata_json JSON NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
UNIQUE KEY uk_salary_item_component (app_code, cycle_id, receiver_user_id, salary_component, relation_scope_key)
);
salary_item_sources(
salary_item_id BIGINT NOT NULL,
source_salary_item_id BIGINT NOT NULL,
source_host_user_id BIGINT NOT NULL,
source_amount_usd_minor BIGINT NOT NULL,
PRIMARY KEY (salary_item_id, source_salary_item_id)
);
relation_scope_key 必须非空,例如 agency:{agency_id}、bd:{bd_user_id}、leader:{leader_user_id}、none:{region_id}。不要把 nullable agency_id 或 bd_user_id 直接放进唯一键后期待 MySQL 帮你保证幂等。
Salary Cycle State
stateDiagram-v2
[*] --> open
open --> calculating
calculating --> draft
draft --> approved
approved --> posting
posting --> posted
posting --> post_failed
post_failed --> posting
draft --> voided
posted --> adjusted
| Status | Meaning |
|---|---|
open |
周期已创建但未计算 |
calculating |
正在生成工资项 |
draft |
已计算,等待后台审核 |
approved |
已审核,可以入账 |
posting |
正在调用钱包入账 |
posted |
全部工资项已入账 |
post_failed |
部分或全部入账失败,可重试 |
voided |
入账前作废 |
adjusted |
已通过调整项修正 |
posted cycle 不可变。迟到事件和人工修正只能创建 adjustment cycle 或 adjustment salary item。
Salary Calculation Rules
Host Salary
输入来自 App 文档定义的 host_daily_stats。基础麦上在线事实先按所有用户沉淀,详见 User Mic Online Time Architecture;host_daily_stats 是工资需要的关系快照派生表。
- Sum
mic_online_mswithin cycle。 - Sum
gift_point_receivedwithin cycle。 - Sum
effective_day。 - 使用 stats 中的
agency_id/bd_user_id/bd_leader_user_id关系快照做归属。
规则匹配:
find highest host policy rule where:
mic_minutes >= min_mic_minutes
gift_points >= min_gift_points
effective_days >= min_effective_days
结果:
- 命中的规则产生
host_salaryitem。 amount_usd_minor = fixed_salary_usd_minor。- 未命中规则时金额为 0;是否保留 0 元 draft item 由后台报表需求决定。
Agency Salary
Agency owner 有两个收益组件:
host_salary: owner 自己作为 host 工作产生。agency_salary: owner 作为 Agency 获得的额外收益。
默认 Agency 基数使用同周期、同 Agency 下所有 host salary item:
agency_base = sum(host_salary.amount_usd_minor where agency_id = agency.agency_id)
agency_salary = rule.fixed_salary + agency_base * rule.commission_bps / 10000
如果产品需要按礼物积分、有效主播数或开播天数计算 Agency 工资,必须把输入字段显式写进 rule 和 item snapshot,不允许结算时从当前 membership 反推。
BD And BD Leader Salary
BD Leader 也是 BD,所以二者使用同一个 bd_salary 组件。receiver_role 区分 bd 和 bd_leader;同一 receiver 和同一来源 host salary 不能同时生成 BD item 和 Leader item。
普通 BD 基数:
bd_source_items = distinct host_salary items where bd_user_id = bd.user_id
bd_base = sum(bd_source_items.amount_usd_minor)
bd_salary = bd_base * bd_policy.commission_bps / 10000 + fixed_salary
BD Leader 基数:
leader_source_items = distinct host_salary items where
bd_user_id = leader.user_id
or bd_leader_user_id = leader.user_id
leader_base = sum(leader_source_items.amount_usd_minor)
leader_salary = leader_base * bd_policy.commission_bps / 10000 + fixed_salary
Leader 直接拥有 Agency 下线时,推荐快照同时设置 bd_user_id = leader.user_id 和 bd_leader_user_id = leader.user_id;工资生成按 source_host_salary.salary_item_id 去重,所以这批 host 对该 leader 只算一次。下级 BD 拥有的 Agency 则设置 bd_user_id = bd.user_id、bd_leader_user_id = leader.user_id。
Posting To Wallet
工资入账必须通过 wallet-service,不能直接改钱包表。
- Admin 审批 cycle。
user-servicehost domain 查询所有approvedsalary items。- 对每个 item 调用
wallet-service CreditSalaryBalance,幂等键为salary:{salary_item_id}。 wallet-servicecreditUSD_BALANCE并返回wallet_transaction_id。user-servicehost domain 保存wallet_transaction_id,把 item 标记为posted。- 如果中途失败,只重试未 posted item,幂等键保持不变。
Wallet metadata 必须包含 cycle_id、salary_item_id、receiver_role、salary_component、policy_id、policy_version、period_start_ms、period_end_ms。
Adjustments
调整只能补差或冲正,不直接修改 posted item。
salary_adjustments(
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
adjustment_id BIGINT NOT NULL PRIMARY KEY,
source_cycle_id BIGINT NOT NULL,
source_salary_item_id BIGINT NULL,
receiver_user_id BIGINT NOT NULL,
amount_usd_minor BIGINT NOT NULL,
reason VARCHAR(512) NOT NULL,
status VARCHAR(32) NOT NULL,
created_by_user_id BIGINT NOT NULL,
approved_by_user_id BIGINT NULL,
wallet_transaction_id VARCHAR(96) NOT NULL DEFAULT '',
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL
);
调整项也必须走审批和钱包幂等入账。负数调整如果钱包账户余额不足,不能直接扣成负数;应该进入人工处理状态,由提现审核或后续工资抵扣策略处理。
Admin APIs
External HTTP stays in gateway-service and must use /api/v1 envelope.
POST /api/v1/admin/host/policies
PUT /api/v1/admin/host/policies/{policy_id}
POST /api/v1/admin/host/policies/{policy_id}/enable
POST /api/v1/admin/host/policies/{policy_id}/disable
POST /api/v1/admin/salary-cycles/calculate
GET /api/v1/admin/salary-cycles
GET /api/v1/admin/salary-cycles/{cycle_id}
GET /api/v1/admin/salary-cycles/{cycle_id}/items
POST /api/v1/admin/salary-cycles/{cycle_id}/approve
POST /api/v1/admin/salary-cycles/{cycle_id}/post
POST /api/v1/admin/salary-cycles/{cycle_id}/void
POST /api/v1/admin/salary-items/{salary_item_id}/adjust
POST /api/v1/admin/bd-leaders
POST /api/v1/admin/bds
POST /api/v1/admin/agencies
POST /api/v1/admin/agencies/{agency_id}/close
POST /api/v1/admin/agencies/{agency_id}/join-enabled
Admin APIs require separate admin auth and audit. App users must never call admin policy or settlement endpoints.
Internal gRPC Sketch
service UserHostAdminService {
rpc CreateSalaryPolicy(CreateSalaryPolicyRequest) returns (CreateSalaryPolicyResponse);
rpc UpdateSalaryPolicy(UpdateSalaryPolicyRequest) returns (UpdateSalaryPolicyResponse);
rpc SetSalaryPolicyStatus(SetSalaryPolicyStatusRequest) returns (SetSalaryPolicyStatusResponse);
rpc CalculateSalaryCycle(CalculateSalaryCycleRequest) returns (CalculateSalaryCycleResponse);
rpc GetSalaryCycle(GetSalaryCycleRequest) returns (GetSalaryCycleResponse);
rpc ApproveSalaryCycle(ApproveSalaryCycleRequest) returns (ApproveSalaryCycleResponse);
rpc PostSalaryCycle(PostSalaryCycleRequest) returns (PostSalaryCycleResponse);
rpc AdjustSalaryItem(AdjustSalaryItemRequest) returns (AdjustSalaryItemResponse);
rpc CreateBDLeader(CreateBDLeaderRequest) returns (CreateBDLeaderResponse);
rpc CreateBD(CreateBDRequest) returns (CreateBDResponse);
rpc CreateAgency(CreateAgencyRequest) returns (CreateAgencyResponse);
rpc CloseAgency(CloseAgencyRequest) returns (CloseAgencyResponse);
}
CreateBDLeaderRequest 必须携带 region_id。user-service 校验该区域 active,锁定目标用户行,必要时更新 users.region_id,再创建 bd_profiles;同一个 command_id 重试只返回已创建的 BD Leader 事实,不重复迁移。
这属于允许改变用户区域的后台关系管理路径之一;完整来源见 User Region Change Sources。
当前项目仍在开发阶段,proto 可以按当前事实调整;修改 api/proto 后必须运行 make proto 并提交生成文件。
Concurrency And Idempotency
- Admin 命令必须携带
command_id,同一command_id重试返回同一结果。 - Policy enable 必须锁定同区域、同 type 的 active policy,防止同时启用多个冲突政策。
- Cycle create 使用唯一键
(region_id, cycle_type, period_start_ms, period_end_ms)。 - Cycle calculation 使用 deterministic item key。重跑同一个
draftcycle 可以替换 draft items 或返回现有结果,不能重复插入。 - Cycle approve 必须锁 cycle 当前状态为
draft。 - Posting 使用
salary_item_id作为钱包幂等键。post_failed重试只处理未 posted item。 - Relationship admin action 要锁目标用户的
users、host_profiles、agencies、bd_profiles,避免和 App 邀请接受并发造成身份冲突。后台创建 BD Leader 需要在同一事务内更新users.region_id和bd_profiles.region_id。
Event And Outbox
user-service host domain should write host outbox rows in the user database:
| Event | When |
|---|---|
SalaryPolicyChanged |
policy created, enabled, disabled, or updated |
SalaryCycleCalculated |
draft generated |
SalaryCycleApproved |
cycle approved |
SalaryCyclePosted |
all salary items posted to wallet |
SalaryItemPosted |
one salary item credited to wallet |
SalaryAdjustmentCreated |
adjustment draft created |
SalaryAdjustmentPosted |
adjustment credited or reversed in wallet |
AdminHostRelationChanged |
admin creates/stops BD/Agency/Leader |
Outbox consumers can update App message inbox, BI, risk systems, and export jobs. They must not be required for salary facts to commit.
App Message Notifications
后台关系和工资动作需要通知 App 用户时,统一进入 App 消息 tab 的 system 分区,具体 message inbox 设计见 App Message Tab Architecture。hyapp-admin-server 只负责后台权限和审计;App 消息事实由 message inbox owner 落库。
| Admin/Salary Fact | Target User | Message Type | Action |
|---|---|---|---|
| Admin creates BD Leader | target user | system |
bd_profile_detail |
| Admin creates BD | target user | system |
bd_profile_detail |
| Admin creates Agency | owner user | system |
agency_detail |
| Agency closed by admin | owner and active members | system |
agency_detail |
| Salary cycle item posted | salary receiver | system |
salary_item_detail |
| Salary adjustment posted | affected receiver | system |
salary_adjustment_detail |
| Salary posting failed after approval | no App user notification by default | internal alert |
工资、关系和钱包入账事实不依赖消息投递。message inbox 消费失败时只能重试或告警,不能回滚 salary cycle、salary item 或 wallet transaction。
Edge Cases
| Case | Rule |
|---|---|
| Policy changes mid-cycle | Existing open/draft cycle keeps selected policy snapshot; future cycles use new active policy |
| Host changes Agency during cycle | Salary uses host_daily_stats relation snapshot and can split across agencies |
| Late room event after posted cycle | Create adjustment item; do not mutate posted salary item |
| Wallet posting partially fails | Retry unposted items with same idempotency key |
| Admin accidentally posts twice | Wallet idempotency must prevent duplicate USD_BALANCE credit |
| Agency closed mid-cycle | Work before close remains attributable; future App joins/search stop immediately |
| User region changed | Existing cycle uses event snapshots; new policy/cycle matching follows stats region |
| Negative adjustment exceeds balance | Move to manual handling; do not force wallet negative unless wallet explicitly supports debt |
Implementation Order
整体开发顺序单独维护在 Host Agency BD Implementation Sequence。Admin 文档只保留后台政策、工资、入账、调整和权限审计设计,避免和 App 实施顺序混在一起。
Verification Matrix
| Scenario | Expected |
|---|---|
| Enable two host policies for same region | Second enable rejected or first disabled in same transaction |
| Calculate daily cycle twice | Same draft result; no duplicate salary items |
| Host meets highest rule | Highest matching level selected |
| Agency salary generated | Base uses host salary items under that Agency only |
| BD Leader owns direct Agency | Source host salary counted once for the leader |
| BD under Leader owns Agency | Host salary contributes to BD and leader according to snapshot |
| Cycle posted twice | Wallet idempotency prevents duplicate USD_BALANCE credit |
| Posting fails after partial success | Retry posts only unposted items |
| Posted item needs correction | Adjustment item created; original item remains immutable |
| Admin closes Agency | Agency hidden from App search; existing facts remain auditable |
Critical Rules
- Admin owns policies and salary cycles; App owns user-initiated relationship flows.
- Wallet is the only owner of
USD_BALANCEand withdrawal state. - Posted salary items are immutable.
- BD/BD Leader base is downstream host salary only.
- Leader direct Agency salary source must be de-duplicated.
- Every admin mutation must be audited and idempotent.
- Redis cannot be the source of truth for policies, salary, relationships, or audit.