"REW quarterly P3 distribution from BP fund — CEO+CFO sign-off + LEARN-007 VP share splits + debit BP balances"
§1 — Description (BCP-14 normative)
The REW service MUST ship quarterly P3 distribution at services/rew/src/p3/ consuming TASK-LEARN-007 VP shares + dual-sign + BP debit + P3 add to next payroll, 5 memory audit kinds.
- MUST validate
distribution_statusagainst closed enum per DEC-2221.
- MUST calculate at
calculator.rs::calc(tenant, quarter, fund_vnd)per DEC-2220:
- Receive VP shares from TASK-LEARN-007 handoff (sums to 1.0)
- Per member: payout_vnd = fund_vnd * vp_share
- MUST require CEO + CFO dual-sign at
dual_sign_gate.rs::can_execute(distribution)per DEC-2222 — same-person rejected.
- MUST be idempotent per DEC-2223 — UNIQUE(tenant, quarter).
- MUST on execute:
- Debit BP balance (TASK-REW-007 credit_p3_distribution converted to BP-equiv)
- Add to next payroll (TASK-REW-005) as P3 income kind
- MUST define tables at migration
0008: ```sql CREATE TABLE rew_p3_distributions ( distribution_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, quarter CHAR(7) NOT NULL, fund_vnd BIGINT NOT NULL CHECK (fund_vnd > 0), status TEXT NOT NULL DEFAULT 'drafted' CHECK (status IN ('drafted','ceo_signed','cfo_signed','executed','paid','dismissed')), ceo_signed_by UUID, ceo_signed_at TIMESTAMPTZ, cfo_signed_by UUID, cfo_signed_at TIMESTAMPTZ, executed_at TIMESTAMPTZ, paid_at TIMESTAMPTZ, trace_id CHAR(32), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (tenant_id, quarter) ); ALTER TABLE rew_p3_distributions ENABLE ROW LEVEL SECURITY; CREATE POLICY p3_dist_rls ON rew_p3_distributions USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); REVOKE UPDATE, DELETE ON rew_p3_distributions FROM cyberos_app; GRANT UPDATE (status, ceo_signed_by, ceo_signed_at, cfo_signed_by, cfo_signed_at, executed_at, paid_at) ON rew_p3_distributions TO cyberos_app;
CREATE TABLE rew_p3_member_payouts ( payout_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, distribution_id UUID NOT NULL REFERENCES rew_p3_distributions(distribution_id), member_id UUID NOT NULL, vp_share NUMERIC(10,9) NOT NULL, payout_vnd BIGINT NOT NULL, PRIMARY KEY_ALT UNIQUE (distribution_id, member_id) ); ALTER TABLE rew_p3_member_payouts ENABLE ROW LEVEL SECURITY; CREATE POLICY payouts_rls ON rew_p3_member_payouts USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); REVOKE UPDATE, DELETE ON rew_p3_member_payouts FROM cyberos_app; ```
- MUST expose endpoints: ``
text POST /v1/rew/p3-distributions (CEO drafts; provides fund_vnd) POST /v1/rew/p3-distributions/{id}/ceo-sign POST /v1/rew/p3-distributions/{id}/cfo-sign POST /v1/rew/p3-distributions/{id}/execute (auto if both signed) GET /v1/rew/p3-distributions/{id} (status + payouts)``
- MUST emit 5 memory audit kinds per DEC-2224. PII per TASK-MEMORY-111: payout amounts SHA256.
- MUST thread trace_id from draft → sign → execute → audit.
- MUST NOT execute without dual-sign per DEC-2222.
- MUST NOT duplicate quarter per DEC-2223.
§2 — Why this design
Why VP share input (DEC-2220)? Fairness — distribution proportional to contribution per TASK-LEARN-007.
Why CEO + CFO dual-sign (DEC-2222)? CEO sets strategic fund; CFO confirms financial OK.
Why idempotent (DEC-2223)? Quarter is single financial event; double-pay = disaster.
§3 — API contract
Sample distribution draft:
POST /v1/rew/p3-distributions
{
"quarter": "2026-Q2",
"fund_vnd": 500000000
}
Sample executed status:
{
"distribution_id": "uuid",
"quarter": "2026-Q2",
"fund_vnd": 500000000,
"status": "executed",
"payouts": [
{"member_id": "uuid", "vp_share": 0.085, "payout_vnd": 42500000},
{"member_id": "uuid", "vp_share": 0.062, "payout_vnd": 31000000}
]
}
§4 — Acceptance criteria
- distribution_status enum cardinality 6. 2. fund_vnd > 0 CHECK. 3. VP shares from TASK-LEARN-007. 4. CEO+CFO dual-sign required. 5. Same-person rejected. 6. UNIQUE(tenant, quarter). 7. 5 memory audit kinds emitted. 8. PII scrubbed (amounts SHA256). 9. RLS denies cross-tenant. 10. CEO-only draft. 11. Trace_id preserved. 12. Append-only via REVOKE except status cols. 13. bigint VND. 14. vp_share NUMERIC(10,9). 15. Per-member payout row. 16. Execute debits BP ledger. 17. P3 added to next payroll. 18. Status workflow enforced. 19. Dismiss allowed pre-execute. 20. Sum of payouts ≈ fund_vnd (±1 VND tolerance).
§5 — Verification
#[tokio::test]
async fn dual_sign_required_for_execute() {
let ctx = TestContext::with_drafted_distribution().await;
ctx.ceo_sign(ctx.dist_id).await;
let r = ctx.try_execute(ctx.dist_id).await;
assert!(r.is_err()); // CFO missing
ctx.cfo_sign(ctx.dist_id).await;
let r2 = ctx.execute(ctx.dist_id).await;
assert!(r2.is_ok());
}
#[tokio::test]
async fn payouts_sum_matches_fund() {
let ctx = TestContext::with_executed_distribution(dec!(500_000_000)).await;
let payouts = ctx.fetch_payouts(ctx.dist_id).await;
let total: i64 = payouts.iter().map(|p| p.payout_vnd).sum();
assert!((total - 500_000_000).abs() <= 1);
}
#[tokio::test]
async fn idempotent_quarter() {
let ctx = TestContext::with_q1_distribution().await;
let r = ctx.try_draft_distribution("2026-Q1").await;
assert!(r.is_err()); // UNIQUE
}
// 5.4..5.10
§7 — Dependencies
Upstream: TASK-REW-007. Cross-module: TASK-LEARN-007 (VP shares), TASK-REW-005 (payroll injection), TASK-AUTH-101 (CEO/CFO), TASK-MCP-007 (trigger cron), TASK-MEMORY-111 (PII).
§10 — Failure modes
| Failure | Detection | Outcome | Recovery |
|---|---|---|---|
| TASK-LEARN-007 not emitted | catch | sev-2; await | manual trigger |
| One signer missing | gate | reject execute | wait |
| Same-person dual-sign | validate | 403 | different signer |
| Sum of payouts != fund | post-condition | sev-1; reject | bug fix |
| Duplicate quarter | UNIQUE | 409 | use different |
| BP debit fail | rollback | sev-1 | retry |
| Payroll injection fail | sev-1 | manual fix | inherent |
| Cross-tenant query | RLS | 0 rows | inherent |
| Decimal precision | bigint VND + rust_decimal share | inherent | inherent |
| Concurrent execute | UPDATE WHERE pending | first wins | inherent |
§11 — Implementation notes
- §11.1 Calculator: per_member_vnd = fund_vnd * vp_share; round to nearest VND; allocate residual to highest-share member to ensure sum matches.
- §11.2 BP debit converts VND to BP-equiv via TASK-HR-005 policy
bp_vnd_rate. - §11.3 memory audit body: distribution_id, quarter, members_count; amounts SHA256.
- §11.4 Cron trigger: quarter+1d via TASK-MCP-007 reminding CEO to draft.
- §11.5 Drafted state allows CEO to revise fund_vnd before signing.
End of TASK-REW-008 spec.