5.0 KiB
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 = ADJUSTMENTsourceType = ADJUSTMENTsourceId = adjustmentIdorderId = nullamountcan be positive or negativeunlockTime = 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
withdrawingearnings lines back toavailable/frozen
Authorization Model (New Endpoints)
Authorization is two-layer:
- Action-level permission: does the user have permission to call the endpoint?
- 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.idgroup.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-Tenantdoes not match the target entity’stenantId, 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:
{
"clerkId": "clerk-id",
"amount": "20.00",
"reasonType": "MANUAL",
"reasonDescription": "text",
"effectiveTime": "2026-01-01T12:00:00" // optional
}
Validation rules:
Idempotency-KeyrequiredtenantIdrequiredclerkIdrequiredamountmust be non-zero (positive = reward-like, negative = punishment-like)reasonTyperequired (currently hard-coded enum values, extend later)reasonDescriptionrequired, non-blank
Idempotency behavior:
- Same
tenantId + Idempotency-Keywith the same request body returns the sameadjustmentId. - Same
tenantId + Idempotency-Keywith 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:
{
"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
statusis one of:PROCESSING: accepted but not yet appliedAPPLIED: earnings line has been createdFAILED: apply failed (and should be visible for operator debugging)
Stress / eventual consistency note:
- Under load (DB latency / executor backlog), polling may stay in
PROCESSINGlonger, but must not create duplicate earnings lines.
Withdrawal Reject API
Reject Withdrawal Request
POST /admin/withdraw/requests/{id}/reject
Body:
{ "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(orrejecteddepending on legacy naming) - all earnings lines with:
withdrawalId = requestIdstatus = withdrawingare released:withdrawalIdset tonull- if
unlockTime > now->status = frozen - else ->
status = available
- request status transitions to
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
unlockTimewindow (equivalent toeffectiveTime)