Files
peipei-backend/llm/earnings-adjustments-and-withdrawal-reject.md

178 lines
5.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Earnings Adjustments & Withdrawal Reject — Expected Behavior
This document defines the intended behavior for:
- Admin-created **earnings adjustments** (positive or negative earning lines)
- Admin **withdrawal reject** (cancel a withdrawal request and release reserved earning lines)
- **Authorization** rules (permission + group leader scope + cross-tenant isolation)
## Concepts
### Earnings Line
An `earnings line` is an immutable money movement entry for a clerk. Amount can be positive or negative.
### Adjustment
An `adjustment` is an admin-originated earnings line, designed to support future extensibility (many “reasons”, auditability, idempotency, async apply).
Key semantics:
- It **creates exactly one** earnings line when applied.
- The created earnings line uses:
- `earningType = ADJUSTMENT`
- `sourceType = ADJUSTMENT`
- `sourceId = adjustmentId`
- `orderId = null`
- `amount` can be positive or negative
- `unlockTime = effectiveTime` (adjustments are effective at their “unlock” time)
### Withdrawal Reject
Admin reject is a cancel operation that:
- marks the withdrawal request as canceled/rejected
- releases reserved `withdrawing` earnings lines back to `available` / `frozen`
## Authorization Model (New Endpoints)
Authorization is **two-layer**:
1) **Action-level permission**: does the user have permission to call the endpoint?
2) **Object-level scope**: can the user act on the target clerk / request?
### Permission Strings
- Create adjustment: `withdraw:adjustment:create`
- Read/poll adjustment status: `withdraw:adjustment:read`
- Reject withdrawal request: `withdraw:request:reject`
### Group Leader Scope
If the current user is **not** `superAdmin`, they can only act on clerks that belong to a group where:
- `clerk.groupId = group.id`
- `group.sysUserId = currentUserId`
If this scope check fails, the endpoint returns **HTTP 403**.
### Super Admin Bypass
If `superAdmin == true`, the user bypasses permission checks and scope checks for these new endpoints.
### Cross-Tenant Isolation
All operations are tenant-scoped.
- If `X-Tenant` does not match the target entitys `tenantId`, the API returns **HTTP 404** (do not leak existence across tenants).
## Admin Earnings Adjustments API
### Create Adjustment
`POST /admin/earnings/adjustments`
Headers:
- `Idempotency-Key: <uuid>` (required)
- `X-Tenant: <tenantId>` (required)
Body:
```json
{
"clerkId": "clerk-id",
"amount": "20.00",
"reasonType": "MANUAL",
"reasonDescription": "text",
"effectiveTime": "2026-01-01T12:00:00" // optional
}
```
Validation rules:
- `Idempotency-Key` required
- `tenantId` required
- `clerkId` required
- `amount` must be non-zero (positive = reward-like, negative = punishment-like)
- `reasonType` required (currently hard-coded enum values, extend later)
- `reasonDescription` required, non-blank
Idempotency behavior:
- Same `tenantId + Idempotency-Key` with the **same request body** returns the **same** `adjustmentId`.
- Same `tenantId + Idempotency-Key` with a **different request body** returns **HTTP 409**.
Response behavior:
- Always returns **HTTP 202 Accepted** on success (request is “in-progress”).
- Includes `Location: /admin/earnings/adjustments/idempotency/{Idempotency-Key}` for polling.
Response example:
```json
{
"code": 202,
"message": "请求处理中",
"data": {
"adjustmentId": "adj-uuid",
"idempotencyKey": "same-key",
"status": "PROCESSING"
}
}
```
### Poll Adjustment Status
`GET /admin/earnings/adjustments/idempotency/{key}`
Behavior:
- If not found in this tenant: **HTTP 404**
- If found:
- returns **HTTP 200**
- `status` is one of:
- `PROCESSING`: accepted but not yet applied
- `APPLIED`: earnings line has been created
- `FAILED`: apply failed (and should be visible for operator debugging)
Stress / eventual consistency note:
- Under load (DB latency / executor backlog), polling may stay in `PROCESSING` longer, but must not create duplicate earnings lines.
## Withdrawal Reject API
### Reject Withdrawal Request
`POST /admin/withdraw/requests/{id}/reject`
Body:
```json
{ "reason": "text (optional)" }
```
Behavior:
- If request does not exist in this tenant: **HTTP 404**
- If request is already canceled/rejected: return **HTTP 200** (idempotent)
- If request is `success`: return **HTTP 400** (cannot reject a successful payout)
- Otherwise:
- request status transitions to `canceled` (or `rejected` depending on legacy naming)
- all earnings lines with:
- `withdrawalId = requestId`
- `status = withdrawing`
are released:
- `withdrawalId` set to `null`
- if `unlockTime > now` -> `status = frozen`
- else -> `status = available`
## Stats: includeAdjustments toggle
The statistics endpoint supports a toggle `includeAdjustments`:
- when `includeAdjustments = false` (default): only order-derived earnings contribute
- when `includeAdjustments = true`: adjustment earnings lines (`sourceType=ADJUSTMENT`) are included in the revenue sum
Time-window behavior:
- adjustment inclusion is based on `unlockTime` window (equivalent to `effectiveTime`)