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

5.0 KiB
Raw Permalink Blame History

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:

{
  "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:

{
  "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:

{ "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)