子币商以及 充值流水相关
This commit is contained in:
parent
c7ca4f23ec
commit
cf98d5bd9a
@ -1622,6 +1622,129 @@
|
||||
"x-permissions": ["coin-seller:update"]
|
||||
}
|
||||
},
|
||||
"/admin/coin-sellers/{user_id}/sub-sellers": {
|
||||
"get": {
|
||||
"operationId": "listCoinSellerSubSellers",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "user_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/Page"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/PageSize"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/CoinSellerPageResponse"
|
||||
}
|
||||
},
|
||||
"x-permission": "coin-seller:view",
|
||||
"x-permissions": ["coin-seller:view"]
|
||||
}
|
||||
},
|
||||
"/admin/coin-seller-sub-applications": {
|
||||
"get": {
|
||||
"operationId": "listCoinSellerSubApplications",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/Page"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/PageSize"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/Keyword"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "status",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": ["pending", "approved", "rejected"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "parent_user_id",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "target_user_id",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/CoinSellerSubApplicationPageResponse"
|
||||
}
|
||||
},
|
||||
"x-permission": "coin-seller-sub-application:view",
|
||||
"x-permissions": ["coin-seller-sub-application:view"]
|
||||
}
|
||||
},
|
||||
"/admin/coin-seller-sub-applications/{application_id}/approve": {
|
||||
"post": {
|
||||
"operationId": "approveCoinSellerSubApplication",
|
||||
"requestBody": {
|
||||
"$ref": "#/components/requestBodies/CoinSellerSubApplicationReviewRequest"
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/CoinSellerSubApplicationReviewResponse"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "application_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"x-permission": "coin-seller-sub-application:audit",
|
||||
"x-permissions": ["coin-seller-sub-application:audit"]
|
||||
}
|
||||
},
|
||||
"/admin/coin-seller-sub-applications/{application_id}/reject": {
|
||||
"post": {
|
||||
"operationId": "rejectCoinSellerSubApplication",
|
||||
"requestBody": {
|
||||
"$ref": "#/components/requestBodies/CoinSellerSubApplicationReviewRequest"
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/CoinSellerSubApplicationReviewResponse"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "application_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"x-permission": "coin-seller-sub-application:audit",
|
||||
"x-permissions": ["coin-seller-sub-application:audit"]
|
||||
}
|
||||
},
|
||||
"/admin/coin-sellers/{user_id}/stock-credits": {
|
||||
"post": {
|
||||
"operationId": "creditCoinSellerStock",
|
||||
@ -2887,7 +3010,12 @@
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": ["admin_stock_credit", "seller_transfer", "sub_seller_transfer", "salary_transfer_received"]
|
||||
"enum": [
|
||||
"admin_stock_credit",
|
||||
"seller_transfer",
|
||||
"sub_seller_transfer",
|
||||
"salary_transfer_received"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -2975,7 +3103,12 @@
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": ["admin_stock_credit", "seller_transfer", "sub_seller_transfer", "salary_transfer_received"]
|
||||
"enum": [
|
||||
"admin_stock_credit",
|
||||
"seller_transfer",
|
||||
"sub_seller_transfer",
|
||||
"salary_transfer_received"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -3083,7 +3216,11 @@
|
||||
{ "in": "query", "name": "keyword", "schema": { "type": "string" } },
|
||||
{ "in": "query", "name": "status", "schema": { "type": "string", "enum": ["active", "disabled"] } },
|
||||
{ "in": "query", "name": "page", "schema": { "type": "integer", "minimum": 1 } },
|
||||
{ "in": "query", "name": "page_size", "schema": { "type": "integer", "minimum": 1, "maximum": 100 } }
|
||||
{
|
||||
"in": "query",
|
||||
"name": "page_size",
|
||||
"schema": { "type": "integer", "minimum": 1, "maximum": 100 }
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
@ -3161,7 +3298,11 @@
|
||||
{ "in": "query", "name": "keyword", "schema": { "type": "string" } },
|
||||
{ "in": "query", "name": "status", "schema": { "type": "string", "enum": ["active", "disabled"] } },
|
||||
{ "in": "query", "name": "page", "schema": { "type": "integer", "minimum": 1 } },
|
||||
{ "in": "query", "name": "page_size", "schema": { "type": "integer", "minimum": 1, "maximum": 100 } }
|
||||
{
|
||||
"in": "query",
|
||||
"name": "page_size",
|
||||
"schema": { "type": "integer", "minimum": 1, "maximum": 100 }
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
@ -3718,7 +3859,12 @@
|
||||
{ "in": "query", "name": "end_ms", "schema": { "type": "integer", "format": "int64" } },
|
||||
{ "in": "query", "name": "app_codes", "schema": { "type": "string" } },
|
||||
{ "in": "query", "name": "region_id", "schema": { "type": "integer", "format": "int64" } },
|
||||
{ "in": "query", "name": "region_ids", "description": "逗号分隔的大区 ID;用于 Social BI 顶栏多选区域。", "schema": { "type": "string" } },
|
||||
{
|
||||
"in": "query",
|
||||
"name": "region_ids",
|
||||
"description": "逗号分隔的大区 ID;用于 Social BI 顶栏多选区域。",
|
||||
"schema": { "type": "string" }
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "section",
|
||||
@ -6728,6 +6874,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CoinSellerSubApplicationReviewRequest": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CoinSellerSubApplicationReviewPayload"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"CountryCreateRequest": {
|
||||
"required": true,
|
||||
"content": {
|
||||
@ -7061,6 +7217,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CoinSellerSubApplicationPageResponse": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponseCoinSellerSubApplicationPage"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"CoinSellerSubApplicationReviewResponse": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponseCoinSellerSubApplicationReview"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"CoinSellerLedgerPageResponse": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
@ -7517,6 +7693,27 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ApiPageCoinSellerSubApplication": {
|
||||
"type": "object",
|
||||
"required": ["items", "page", "pageSize", "total"],
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/CoinSellerSubApplication"
|
||||
}
|
||||
},
|
||||
"page": {
|
||||
"type": "integer"
|
||||
},
|
||||
"pageSize": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ApiPageCoinSellerLedger": {
|
||||
"type": "object",
|
||||
"required": ["items", "page", "pageSize", "total"],
|
||||
@ -7894,6 +8091,36 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ApiResponseCoinSellerSubApplicationPage": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Envelope"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/ApiPageCoinSellerSubApplication"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ApiResponseCoinSellerSubApplicationReview": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Envelope"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/CoinSellerSubApplicationReviewResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ApiResponseCoinSellerLedgerPage": {
|
||||
"allOf": [
|
||||
{
|
||||
@ -8780,6 +9007,9 @@
|
||||
"type": "string",
|
||||
"enum": ["active", "disabled"]
|
||||
},
|
||||
"subCoinSellerCount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"totalRechargeUsdtMicro": {
|
||||
"type": "integer"
|
||||
},
|
||||
@ -8794,6 +9024,204 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CoinSellerSubApplicationUser": {
|
||||
"type": "object",
|
||||
"required": ["userId"],
|
||||
"properties": {
|
||||
"avatar": {
|
||||
"type": "string"
|
||||
},
|
||||
"displayUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"regionId": {
|
||||
"type": "integer"
|
||||
},
|
||||
"regionName": {
|
||||
"type": "string"
|
||||
},
|
||||
"userId": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CoinSellerSubApplication": {
|
||||
"type": "object",
|
||||
"required": ["applicationId", "parent", "target", "status"],
|
||||
"properties": {
|
||||
"applicationId": {
|
||||
"type": "string"
|
||||
},
|
||||
"commandId": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdAtMs": {
|
||||
"type": "integer"
|
||||
},
|
||||
"parent": {
|
||||
"$ref": "#/components/schemas/CoinSellerSubApplicationUser"
|
||||
},
|
||||
"relationId": {
|
||||
"type": "string"
|
||||
},
|
||||
"reviewedAtMs": {
|
||||
"type": "integer"
|
||||
},
|
||||
"reviewedByAdminId": {
|
||||
"type": "string"
|
||||
},
|
||||
"reviewReason": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["pending", "approved", "rejected"]
|
||||
},
|
||||
"target": {
|
||||
"$ref": "#/components/schemas/CoinSellerSubApplicationUser"
|
||||
},
|
||||
"updatedAtMs": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CoinSellerSubApplicationReviewApplication": {
|
||||
"type": "object",
|
||||
"required": ["applicationId", "parentUserId", "targetUserId", "status"],
|
||||
"properties": {
|
||||
"applicationId": {
|
||||
"type": "string"
|
||||
},
|
||||
"commandId": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdAtMs": {
|
||||
"type": "integer"
|
||||
},
|
||||
"parentUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"relationId": {
|
||||
"type": "string"
|
||||
},
|
||||
"reviewedAtMs": {
|
||||
"type": "integer"
|
||||
},
|
||||
"reviewedByAdminId": {
|
||||
"type": "string"
|
||||
},
|
||||
"reviewReason": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["pending", "approved", "rejected"]
|
||||
},
|
||||
"targetUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"updatedAtMs": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CoinSellerSubRelation": {
|
||||
"type": "object",
|
||||
"required": ["relationId", "parentUserId", "childUserId", "status"],
|
||||
"properties": {
|
||||
"childUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdAtMs": {
|
||||
"type": "integer"
|
||||
},
|
||||
"createdByUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"parentUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"relationId": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"updatedAtMs": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CoinSellerSubApplicationReviewChild": {
|
||||
"type": "object",
|
||||
"required": ["userId"],
|
||||
"properties": {
|
||||
"avatar": {
|
||||
"type": "string"
|
||||
},
|
||||
"canManageSubCoinSellers": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"contactInfo": {
|
||||
"type": "string"
|
||||
},
|
||||
"displayUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"merchantAssetType": {
|
||||
"type": "string"
|
||||
},
|
||||
"regionId": {
|
||||
"type": "integer"
|
||||
},
|
||||
"regionName": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"updatedAtMs": {
|
||||
"type": "integer"
|
||||
},
|
||||
"userId": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CoinSellerSubApplicationReviewPayload": {
|
||||
"type": "object",
|
||||
"required": ["commandId"],
|
||||
"properties": {
|
||||
"commandId": {
|
||||
"type": "string",
|
||||
"maxLength": 120
|
||||
},
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"maxLength": 200
|
||||
}
|
||||
}
|
||||
},
|
||||
"CoinSellerSubApplicationReviewResult": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"application": {
|
||||
"$ref": "#/components/schemas/CoinSellerSubApplicationReviewApplication"
|
||||
},
|
||||
"child": {
|
||||
"$ref": "#/components/schemas/CoinSellerSubApplicationReviewChild"
|
||||
},
|
||||
"relation": {
|
||||
"$ref": "#/components/schemas/CoinSellerSubRelation"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CoinLedgerUser": {
|
||||
"type": "object",
|
||||
"required": ["userId"],
|
||||
@ -8814,7 +9242,19 @@
|
||||
},
|
||||
"CoinSellerLedger": {
|
||||
"type": "object",
|
||||
"required": ["entryId", "transactionId", "ledgerType", "bizType", "seller", "receiver", "amount", "direction", "availableDelta", "sellerBalanceAfter", "createdAtMs"],
|
||||
"required": [
|
||||
"entryId",
|
||||
"transactionId",
|
||||
"ledgerType",
|
||||
"bizType",
|
||||
"seller",
|
||||
"receiver",
|
||||
"amount",
|
||||
"direction",
|
||||
"availableDelta",
|
||||
"sellerBalanceAfter",
|
||||
"createdAtMs"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "integer",
|
||||
@ -8857,7 +9297,12 @@
|
||||
},
|
||||
"ledgerType": {
|
||||
"type": "string",
|
||||
"enum": ["admin_stock_credit", "seller_transfer", "sub_seller_transfer", "salary_transfer_received"]
|
||||
"enum": [
|
||||
"admin_stock_credit",
|
||||
"seller_transfer",
|
||||
"sub_seller_transfer",
|
||||
"salary_transfer_received"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
@ -10132,6 +10577,13 @@
|
||||
"externalOrderNo": {
|
||||
"type": "string"
|
||||
},
|
||||
"providerCurrencyCode": {
|
||||
"type": "string"
|
||||
},
|
||||
"providerAmountMinor": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"chain": {
|
||||
"type": "string",
|
||||
"enum": ["TRON", "BSC"]
|
||||
@ -10210,6 +10662,13 @@
|
||||
"usdMinorAmount": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"providerCurrencyCode": {
|
||||
"type": "string"
|
||||
},
|
||||
"providerAmountMinor": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -113,14 +113,18 @@ test("finance coin seller recharge order APIs use generated endpoint paths", asy
|
||||
appCode: "lalu",
|
||||
coinAmount: 100000,
|
||||
externalOrderNo: "MIFA-001",
|
||||
providerAmountMinor: 125050,
|
||||
providerCode: "mifapay",
|
||||
providerCurrencyCode: "PHP",
|
||||
targetUserId: "10001",
|
||||
usdAmount: 10
|
||||
});
|
||||
await verifyFinanceCoinSellerRechargeReceipt({
|
||||
appCode: "lalu",
|
||||
externalOrderNo: "MIFA-001",
|
||||
providerAmountMinor: 125050,
|
||||
providerCode: "mifapay",
|
||||
providerCurrencyCode: "PHP",
|
||||
usdAmount: 10
|
||||
});
|
||||
await verifyFinanceCoinSellerRechargeOrder("order-1");
|
||||
@ -132,13 +136,24 @@ test("finance coin seller recharge order APIs use generated endpoint paths", asy
|
||||
expect(calls[0][1]?.method).toBe("GET");
|
||||
expect(String(calls[1][0])).toContain("/api/v1/admin/finance/orders/coin-seller-recharges");
|
||||
expect(calls[1][1]?.method).toBe("POST");
|
||||
expect(JSON.parse(String(calls[1][1]?.body))).toMatchObject({ externalOrderNo: "MIFA-001", providerCode: "mifapay" });
|
||||
expect(JSON.parse(String(calls[1][1]?.body))).toEqual({
|
||||
appCode: "lalu",
|
||||
coinAmount: 100000,
|
||||
externalOrderNo: "MIFA-001",
|
||||
providerAmountMinor: 125050,
|
||||
providerCode: "mifapay",
|
||||
providerCurrencyCode: "PHP",
|
||||
targetUserId: "10001",
|
||||
usdAmount: 10
|
||||
});
|
||||
expect(String(calls[2][0])).toContain("/api/v1/admin/finance/orders/coin-seller-recharges/receipt-verifications");
|
||||
expect(calls[2][1]?.method).toBe("POST");
|
||||
expect(JSON.parse(String(calls[2][1]?.body))).toEqual({
|
||||
appCode: "lalu",
|
||||
externalOrderNo: "MIFA-001",
|
||||
providerAmountMinor: 125050,
|
||||
providerCode: "mifapay",
|
||||
providerCurrencyCode: "PHP",
|
||||
usdAmount: 10
|
||||
});
|
||||
expect(String(calls[3][0])).toContain("/api/v1/admin/finance/orders/coin-seller-recharges/order-1/verify");
|
||||
|
||||
@ -3,6 +3,7 @@ import FactCheckOutlined from "@mui/icons-material/FactCheckOutlined";
|
||||
import PaidOutlined from "@mui/icons-material/PaidOutlined";
|
||||
import RefreshOutlined from "@mui/icons-material/RefreshOutlined";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import Button from "@mui/material/Button";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
@ -14,6 +15,7 @@ import TextField from "@mui/material/TextField";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
COIN_SELLER_RECHARGE_CHAINS,
|
||||
COIN_SELLER_RECHARGE_PROVIDER_CURRENCIES,
|
||||
COIN_SELLER_RECHARGE_PROVIDERS,
|
||||
COIN_SELLER_RECHARGE_STATUS,
|
||||
DEFAULT_COIN_SELLER_RECHARGE_ORDER_FORM,
|
||||
@ -78,7 +80,9 @@ export function FinanceCoinSellerRechargeOrderList({
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
chain: "",
|
||||
providerAmount: "",
|
||||
providerCode: value,
|
||||
providerCurrencyCode: "",
|
||||
}));
|
||||
setFormError("");
|
||||
setReceiptVerification(null);
|
||||
@ -201,7 +205,12 @@ export function FinanceCoinSellerRechargeOrderList({
|
||||
<Button startIcon={<RefreshOutlined fontSize="small" />} variant="outlined" onClick={onReload}>
|
||||
刷新
|
||||
</Button>
|
||||
<Button disabled={!canCreate} startIcon={<AddOutlined fontSize="small" />} variant="contained" onClick={openCreate}>
|
||||
<Button
|
||||
disabled={!canCreate}
|
||||
startIcon={<AddOutlined fontSize="small" />}
|
||||
variant="contained"
|
||||
onClick={openCreate}
|
||||
>
|
||||
创建币商充值
|
||||
</Button>
|
||||
</div>
|
||||
@ -234,7 +243,10 @@ export function FinanceCoinSellerRechargeOrderList({
|
||||
<td className="finance-order-cell">{item.id || "-"}</td>
|
||||
<td>{appName(item.appCode, apps)}</td>
|
||||
<td>
|
||||
<Stacked primary={item.targetDisplayUserId || item.targetUserId} secondary={item.targetUserId} />
|
||||
<Stacked
|
||||
primary={item.targetDisplayUserId || item.targetUserId}
|
||||
secondary={item.targetUserId}
|
||||
/>
|
||||
</td>
|
||||
<td className="r num">
|
||||
<Stacked
|
||||
@ -243,10 +255,16 @@ export function FinanceCoinSellerRechargeOrderList({
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Stacked primary={coinSellerRechargeProviderLabel(item.providerCode)} secondary={providerMeta(item)} />
|
||||
<Stacked
|
||||
primary={coinSellerRechargeProviderLabel(item.providerCode)}
|
||||
secondary={providerMeta(item)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Stacked primary={item.externalOrderNo || "-"} secondary={providerAmountText(item)} />
|
||||
<Stacked
|
||||
primary={item.externalOrderNo || "-"}
|
||||
secondary={providerAmountText(item)}
|
||||
/>
|
||||
</td>
|
||||
<td className="finance-remark-cell">{item.remark || "-"}</td>
|
||||
<td>
|
||||
@ -266,7 +284,8 @@ export function FinanceCoinSellerRechargeOrderList({
|
||||
primary={item.operatorName || "-"}
|
||||
secondary={[
|
||||
item.verifiedByName && `校验:${item.verifiedByName}`,
|
||||
item.grantedByName && `${isZeroCoinMakeupOrder(item) ? "补单" : "发放"}:${item.grantedByName}`,
|
||||
item.grantedByName &&
|
||||
`${isZeroCoinMakeupOrder(item) ? "补单" : "发放"}:${item.grantedByName}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" / ")}
|
||||
@ -277,7 +296,8 @@ export function FinanceCoinSellerRechargeOrderList({
|
||||
primary={formatTime(item.createdAtMs)}
|
||||
secondary={[
|
||||
item.verifiedAtMs && `校验 ${formatTime(item.verifiedAtMs)}`,
|
||||
item.grantedAtMs && `${isZeroCoinMakeupOrder(item) ? "补单" : "发放"} ${formatTime(item.grantedAtMs)}`,
|
||||
item.grantedAtMs &&
|
||||
`${isZeroCoinMakeupOrder(item) ? "补单" : "发放"} ${formatTime(item.grantedAtMs)}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" / ")}
|
||||
@ -330,13 +350,21 @@ export function FinanceCoinSellerRechargeOrderList({
|
||||
) : null}
|
||||
{total > pageSize ? (
|
||||
<div className="finance-pagination">
|
||||
<Button disabled={page <= 1 || loading} variant="outlined" onClick={() => onFiltersChange({ page: page - 1 })}>
|
||||
<Button
|
||||
disabled={page <= 1 || loading}
|
||||
variant="outlined"
|
||||
onClick={() => onFiltersChange({ page: page - 1 })}
|
||||
>
|
||||
上一页
|
||||
</Button>
|
||||
<span>
|
||||
第 {page} 页 / 共 {total} 条
|
||||
</span>
|
||||
<Button disabled={!hasNextPage || loading} variant="outlined" onClick={() => onFiltersChange({ page: page + 1 })}>
|
||||
<Button
|
||||
disabled={!hasNextPage || loading}
|
||||
variant="outlined"
|
||||
onClick={() => onFiltersChange({ page: page + 1 })}
|
||||
>
|
||||
下一页
|
||||
</Button>
|
||||
</div>
|
||||
@ -379,14 +407,37 @@ function CreateDialog({
|
||||
const canCreate = receiptVerification?.verified === true && !disabled;
|
||||
|
||||
return (
|
||||
<Dialog fullWidth maxWidth={false} open={open} slotProps={{ paper: { className: "finance-create-dialog-paper finance-create-dialog-paper--coin-seller" } }} onClose={disabled ? undefined : onClose}>
|
||||
<Dialog
|
||||
fullWidth
|
||||
maxWidth={false}
|
||||
open={open}
|
||||
slotProps={{ paper: { className: "finance-create-dialog-paper finance-create-dialog-paper--coin-seller" } }}
|
||||
onClose={disabled ? undefined : onClose}
|
||||
>
|
||||
<DialogTitle>创建币商充值</DialogTitle>
|
||||
<DialogContent className="finance-create-dialog">
|
||||
<form className="finance-form finance-form--dialog" id="coin-seller-recharge-order-form" onSubmit={onSubmit}>
|
||||
<form
|
||||
className="finance-form finance-form--dialog"
|
||||
id="coin-seller-recharge-order-form"
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{formError ? <Alert severity="error">{formError}</Alert> : null}
|
||||
<div className={form.providerCode === "usdt" ? "finance-form-grid finance-form-grid--coin-seller-create" : "finance-form-grid finance-form-grid--coin-seller-create finance-form-grid--coin-seller-no-chain"}>
|
||||
<div
|
||||
className={
|
||||
form.providerCode === "usdt"
|
||||
? "finance-form-grid finance-form-grid--coin-seller-create"
|
||||
: "finance-form-grid finance-form-grid--coin-seller-create finance-form-grid--coin-seller-no-chain"
|
||||
}
|
||||
>
|
||||
{apps.length ? (
|
||||
<TextField disabled={disabled} label="APP" required select value={form.appCode} onChange={(event) => onChange("appCode", event.target.value)}>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
label="APP"
|
||||
required
|
||||
select
|
||||
value={form.appCode}
|
||||
onChange={(event) => onChange("appCode", event.target.value)}
|
||||
>
|
||||
{apps.map((app) => (
|
||||
<MenuItem key={app.appCode} value={app.appCode}>
|
||||
{app.appName || app.appCode}
|
||||
@ -394,9 +445,21 @@ function CreateDialog({
|
||||
))}
|
||||
</TextField>
|
||||
) : (
|
||||
<TextField disabled={disabled} label="APP" required value={form.appCode} onChange={(event) => onChange("appCode", event.target.value)} />
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
label="APP"
|
||||
required
|
||||
value={form.appCode}
|
||||
onChange={(event) => onChange("appCode", event.target.value)}
|
||||
/>
|
||||
)}
|
||||
<TextField disabled={disabled} label="目标用户 ID" required value={form.targetUserId} onChange={(event) => onChange("targetUserId", event.target.value)} />
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
label="目标用户 ID"
|
||||
required
|
||||
value={form.targetUserId}
|
||||
onChange={(event) => onChange("targetUserId", event.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
InputProps={{ startAdornment: <InputAdornment position="start">USD</InputAdornment> }}
|
||||
@ -417,7 +480,14 @@ function CreateDialog({
|
||||
value={form.coinAmount}
|
||||
onChange={(event) => onChange("coinAmount", event.target.value)}
|
||||
/>
|
||||
<TextField disabled={disabled} label="支付渠道" required select value={form.providerCode} onChange={(event) => onChange("providerCode", event.target.value)}>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
label="支付渠道"
|
||||
required
|
||||
select
|
||||
value={form.providerCode}
|
||||
onChange={(event) => onChange("providerCode", event.target.value)}
|
||||
>
|
||||
{COIN_SELLER_RECHARGE_PROVIDERS.map(([value, label]) => (
|
||||
<MenuItem key={value} value={value}>
|
||||
{label}
|
||||
@ -425,7 +495,14 @@ function CreateDialog({
|
||||
))}
|
||||
</TextField>
|
||||
{form.providerCode === "usdt" ? (
|
||||
<TextField disabled={disabled} label="USDT 链" required select value={form.chain} onChange={(event) => onChange("chain", event.target.value)}>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
label="USDT 链"
|
||||
required
|
||||
select
|
||||
value={form.chain}
|
||||
onChange={(event) => onChange("chain", event.target.value)}
|
||||
>
|
||||
{COIN_SELLER_RECHARGE_CHAINS.map(([value, label]) => (
|
||||
<MenuItem key={value} value={value}>
|
||||
{label}
|
||||
@ -433,9 +510,45 @@ function CreateDialog({
|
||||
))}
|
||||
</TextField>
|
||||
) : null}
|
||||
{form.providerCode !== "usdt" ? (
|
||||
<>
|
||||
<Autocomplete
|
||||
disabled={disabled}
|
||||
getOptionLabel={currencyOptionLabel}
|
||||
isOptionEqualToValue={(option, value) => option === value}
|
||||
noOptionsText="无匹配币种"
|
||||
options={providerCurrencyOptions(form.providerCode)}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label="币种" required />
|
||||
)}
|
||||
value={form.providerCurrencyCode || null}
|
||||
onChange={(_, value) => onChange("providerCurrencyCode", value || "")}
|
||||
/>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
inputProps={{ min: 0, step: "0.01" }}
|
||||
label="币种金额"
|
||||
required
|
||||
type="number"
|
||||
value={form.providerAmount}
|
||||
onChange={(event) => onChange("providerAmount", event.target.value)}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
<div className="finance-receipt-field">
|
||||
<TextField disabled={disabled} label="三方订单号" required value={form.externalOrderNo} onChange={(event) => onChange("externalOrderNo", event.target.value)} />
|
||||
<Button disabled={disabled || !onVerifyReceipt} startIcon={<FactCheckOutlined fontSize="small" />} variant="outlined" onClick={onVerifyReceipt}>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
label="三方订单号"
|
||||
required
|
||||
value={form.externalOrderNo}
|
||||
onChange={(event) => onChange("externalOrderNo", event.target.value)}
|
||||
/>
|
||||
<Button
|
||||
disabled={disabled || !onVerifyReceipt}
|
||||
startIcon={<FactCheckOutlined fontSize="small" />}
|
||||
variant="outlined"
|
||||
onClick={onVerifyReceipt}
|
||||
>
|
||||
校验
|
||||
</Button>
|
||||
</div>
|
||||
@ -466,6 +579,14 @@ function isZeroCoinMakeupOrder(order) {
|
||||
return Number(order?.coinAmount || 0) === 0;
|
||||
}
|
||||
|
||||
function providerCurrencyOptions(providerCode) {
|
||||
return COIN_SELLER_RECHARGE_PROVIDER_CURRENCIES[providerCode] || [];
|
||||
}
|
||||
|
||||
function currencyOptionLabel(option) {
|
||||
return option || "";
|
||||
}
|
||||
|
||||
function ReceiptVerificationBanner({ verification }) {
|
||||
const detail = [
|
||||
verification.status && `状态 ${verification.status}`,
|
||||
@ -486,7 +607,11 @@ function ReceiptVerificationBanner({ verification }) {
|
||||
}
|
||||
|
||||
function StatusBadge({ status }) {
|
||||
return <span className={`finance-status finance-status--${coinSellerRechargeStatusTone(status)}`}>{coinSellerRechargeStatusLabel(status)}</span>;
|
||||
return (
|
||||
<span className={`finance-status finance-status--${coinSellerRechargeStatusTone(status)}`}>
|
||||
{coinSellerRechargeStatusLabel(status)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function Stacked({ primary, secondary }) {
|
||||
@ -523,7 +648,9 @@ function providerMeta(item) {
|
||||
return [item.providerStatus, item.providerOrderId].filter(Boolean).join(" / ") || "-";
|
||||
}
|
||||
if (item.providerCode === "v5pay") {
|
||||
return [item.providerStatus, item.providerCurrencyCode, item.providerOrderId].filter(Boolean).join(" / ") || "-";
|
||||
return (
|
||||
[item.providerStatus, item.providerCurrencyCode, item.providerOrderId].filter(Boolean).join(" / ") || "-"
|
||||
);
|
||||
}
|
||||
return "-";
|
||||
}
|
||||
@ -531,8 +658,12 @@ function providerMeta(item) {
|
||||
function walletMeta(item) {
|
||||
return [
|
||||
item.walletAssetType,
|
||||
item.walletAmountDelta === null || item.walletAmountDelta === undefined ? "" : `变动 ${formatAmount(item.walletAmountDelta)}`,
|
||||
item.walletBalanceAfter === null || item.walletBalanceAfter === undefined ? "" : `余额 ${formatAmount(item.walletBalanceAfter)}`,
|
||||
item.walletAmountDelta === null || item.walletAmountDelta === undefined
|
||||
? ""
|
||||
: `变动 ${formatAmount(item.walletAmountDelta)}`,
|
||||
item.walletBalanceAfter === null || item.walletBalanceAfter === undefined
|
||||
? ""
|
||||
: `余额 ${formatAmount(item.walletBalanceAfter)}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" / ");
|
||||
|
||||
@ -9,26 +9,31 @@ afterEach(() => {
|
||||
test("coin seller recharge order create dialog verifies receipt before create", async () => {
|
||||
const onCreate = vi.fn(async () => ({ id: "order-2" }));
|
||||
const onVerifyReceipt = vi.fn(async () => ({
|
||||
currencyCode: "USD",
|
||||
providerAmountMinor: 1000,
|
||||
currencyCode: "PHP",
|
||||
providerAmountMinor: 4800,
|
||||
providerOrderId: "V5-001",
|
||||
status: "paid",
|
||||
verified: true,
|
||||
}));
|
||||
render(<FinanceCoinSellerRechargeOrderList {...baseProps()} onCreate={onCreate} onVerifyReceipt={onVerifyReceipt} />);
|
||||
render(
|
||||
<FinanceCoinSellerRechargeOrderList {...baseProps()} onCreate={onCreate} onVerifyReceipt={onVerifyReceipt} />,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "创建币商充值" }));
|
||||
fireEvent.mouseDown(screen.getByRole("combobox", { name: "支付渠道" }));
|
||||
fireEvent.click(screen.getByRole("option", { name: "V5Pay" }));
|
||||
|
||||
expect(screen.queryByRole("textbox", { name: "三方国家码" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("textbox", { name: "三方币种" })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("combobox", { name: "币种" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("spinbutton", { name: "币种金额" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("textbox", { name: "支付类型" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("textbox", { name: "订单日期" })).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.change(screen.getByRole("textbox", { name: "目标用户 ID" }), { target: { value: "10001" } });
|
||||
fireEvent.change(screen.getByRole("spinbutton", { name: "USD 金额" }), { target: { value: "10" } });
|
||||
fireEvent.change(screen.getByRole("spinbutton", { name: "充值金币" }), { target: { value: "0" } });
|
||||
fireEvent.mouseDown(screen.getByRole("combobox", { name: "币种" }));
|
||||
fireEvent.click(await screen.findByRole("option", { name: "PHP" }));
|
||||
fireEvent.change(screen.getByRole("spinbutton", { name: "币种金额" }), { target: { value: "48" } });
|
||||
fireEvent.change(screen.getByRole("textbox", { name: "三方订单号" }), { target: { value: "V5-001" } });
|
||||
fireEvent.change(screen.getByRole("textbox", { name: "备注" }), { target: { value: "补单,不发金币" } });
|
||||
expect(screen.getByRole("button", { name: "创建" })).toBeDisabled();
|
||||
@ -39,7 +44,9 @@ test("coin seller recharge order create dialog verifies receipt before create",
|
||||
expect(onVerifyReceipt).toHaveBeenCalledWith({
|
||||
appCode: "lalu",
|
||||
externalOrderNo: "V5-001",
|
||||
providerAmountMinor: 4800,
|
||||
providerCode: "v5pay",
|
||||
providerCurrencyCode: "PHP",
|
||||
usdAmount: 10,
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "创建" }));
|
||||
@ -48,7 +55,9 @@ test("coin seller recharge order create dialog verifies receipt before create",
|
||||
appCode: "lalu",
|
||||
coinAmount: 0,
|
||||
externalOrderNo: "V5-001",
|
||||
providerAmountMinor: 4800,
|
||||
providerCode: "v5pay",
|
||||
providerCurrencyCode: "PHP",
|
||||
remark: "补单,不发金币",
|
||||
targetUserId: "10001",
|
||||
usdAmount: 10,
|
||||
@ -59,7 +68,10 @@ test("coin seller recharge order create dialog verifies receipt before create",
|
||||
test("coin seller recharge order actions follow verify and grant status", () => {
|
||||
const onVerify = vi.fn();
|
||||
const onGrant = vi.fn();
|
||||
vi.stubGlobal("confirm", vi.fn(() => true));
|
||||
vi.stubGlobal(
|
||||
"confirm",
|
||||
vi.fn(() => true),
|
||||
);
|
||||
|
||||
render(
|
||||
<FinanceCoinSellerRechargeOrderList
|
||||
@ -100,7 +112,9 @@ test("zero coin seller recharge order uses makeup completion action", () => {
|
||||
<FinanceCoinSellerRechargeOrderList
|
||||
{...baseProps({
|
||||
orders: {
|
||||
items: [orderFixture({ coinAmount: 0, grantStatus: "pending", id: "makeup", verifyStatus: "verified" })],
|
||||
items: [
|
||||
orderFixture({ coinAmount: 0, grantStatus: "pending", id: "makeup", verifyStatus: "verified" }),
|
||||
],
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
total: 1,
|
||||
|
||||
@ -70,6 +70,11 @@ export const COIN_SELLER_RECHARGE_CHAINS = [
|
||||
["BSC", "BSC"],
|
||||
];
|
||||
|
||||
export const COIN_SELLER_RECHARGE_PROVIDER_CURRENCIES = {
|
||||
mifapay: ["PHP", "INR", "IDR", "VND", "SAR", "AED", "BHD", "QAR", "OMR", "KWD", "BDT", "PKR", "EGP"],
|
||||
v5pay: ["PHP", "SAR", "EGP", "AED", "IDR", "INR", "PKR", "TRY", "MXN"],
|
||||
};
|
||||
|
||||
export const COIN_SELLER_RECHARGE_STATUS = [
|
||||
["pending", "待处理"],
|
||||
["verified", "已校验"],
|
||||
@ -106,7 +111,9 @@ export const DEFAULT_COIN_SELLER_RECHARGE_ORDER_FORM = {
|
||||
chain: "",
|
||||
coinAmount: "",
|
||||
externalOrderNo: "",
|
||||
providerAmount: "",
|
||||
providerCode: "mifapay",
|
||||
providerCurrencyCode: "",
|
||||
remark: "",
|
||||
targetUserId: "",
|
||||
usdAmount: "",
|
||||
|
||||
@ -129,12 +129,16 @@ export function normalizeWithdrawalApplicationPage(data = {}) {
|
||||
|
||||
export function buildCoinSellerRechargeOrderPayload(form) {
|
||||
const providerCode = stringValue(form.providerCode);
|
||||
const providerCurrencyCode = stringValue(form.providerCurrencyCode).toUpperCase();
|
||||
const providerAmountMinor = providerAmountMinorFromForm(form);
|
||||
const payload = {
|
||||
appCode: stringValue(form.appCode),
|
||||
chain: providerCode === "usdt" ? stringValue(form.chain).toUpperCase() : "",
|
||||
coinAmount: numberValue(form.coinAmount),
|
||||
externalOrderNo: stringValue(form.externalOrderNo),
|
||||
providerAmountMinor: requiresThirdPartyProviderAmount(providerCode) ? providerAmountMinor : "",
|
||||
providerCode,
|
||||
providerCurrencyCode: requiresThirdPartyProviderAmount(providerCode) ? providerCurrencyCode : "",
|
||||
remark: stringValue(form.remark),
|
||||
targetUserId: stringValue(form.targetUserId),
|
||||
usdAmount: numberValue(form.usdAmount),
|
||||
@ -145,12 +149,16 @@ export function buildCoinSellerRechargeOrderPayload(form) {
|
||||
|
||||
export function buildCoinSellerRechargeReceiptPayload(form) {
|
||||
const providerCode = stringValue(form.providerCode);
|
||||
const providerCurrencyCode = stringValue(form.providerCurrencyCode).toUpperCase();
|
||||
const providerAmountMinor = providerAmountMinorFromForm(form);
|
||||
return Object.fromEntries(
|
||||
Object.entries({
|
||||
appCode: stringValue(form.appCode),
|
||||
chain: providerCode === "usdt" ? stringValue(form.chain).toUpperCase() : "",
|
||||
externalOrderNo: stringValue(form.externalOrderNo),
|
||||
providerAmountMinor: requiresThirdPartyProviderAmount(providerCode) ? providerAmountMinor : "",
|
||||
providerCode,
|
||||
providerCurrencyCode: requiresThirdPartyProviderAmount(providerCode) ? providerCurrencyCode : "",
|
||||
usdAmount: numberValue(form.usdAmount),
|
||||
}).filter(([, value]) => value !== ""),
|
||||
);
|
||||
@ -178,6 +186,14 @@ export function validateCoinSellerRechargeOrderPayload(payload) {
|
||||
if (payload.providerCode === "usdt" && !payload.chain) {
|
||||
return "请选择 USDT 链";
|
||||
}
|
||||
if (requiresThirdPartyProviderAmount(payload.providerCode)) {
|
||||
if (!payload.providerCurrencyCode) {
|
||||
return "请填写币种";
|
||||
}
|
||||
if (!Number.isInteger(payload.providerAmountMinor) || payload.providerAmountMinor <= 0) {
|
||||
return "请填写大于 0 的币种金额";
|
||||
}
|
||||
}
|
||||
if (payload.remark && payload.remark.length > 512) {
|
||||
return "备注不能超过 512 字";
|
||||
}
|
||||
@ -642,6 +658,26 @@ function numberOrNull(value) {
|
||||
return Number.isFinite(nextValue) ? nextValue : null;
|
||||
}
|
||||
|
||||
function providerMoneyToMinor(value) {
|
||||
const number = numberValue(value);
|
||||
if (!Number.isFinite(number)) {
|
||||
return Number.NaN;
|
||||
}
|
||||
return Math.round(number * 100);
|
||||
}
|
||||
|
||||
function providerAmountMinorFromForm(form) {
|
||||
// 表单输入是常规金额,已构建 payload 重用时则直接沿用 minor,避免校验请求二次放大金额。
|
||||
if (stringValue(form.providerAmount) !== "") {
|
||||
return providerMoneyToMinor(form.providerAmount);
|
||||
}
|
||||
return numberValue(form.providerAmountMinor);
|
||||
}
|
||||
|
||||
function requiresThirdPartyProviderAmount(providerCode) {
|
||||
return ["mifapay", "v5pay"].includes(stringValue(providerCode).toLowerCase());
|
||||
}
|
||||
|
||||
function stringValue(value) {
|
||||
return String(value ?? "").trim();
|
||||
}
|
||||
|
||||
@ -170,7 +170,9 @@ test("builds validates and normalizes coin seller recharge orders", () => {
|
||||
appCode: " lalu ",
|
||||
coinAmount: "100000",
|
||||
externalOrderNo: " MIFA-001 ",
|
||||
providerAmount: "1250.5",
|
||||
providerCode: "mifapay",
|
||||
providerCurrencyCode: " php ",
|
||||
remark: " 人工补单 ",
|
||||
targetUserId: " 10001 ",
|
||||
usdAmount: "10.5",
|
||||
@ -180,7 +182,9 @@ test("builds validates and normalizes coin seller recharge orders", () => {
|
||||
appCode: "lalu",
|
||||
coinAmount: 100000,
|
||||
externalOrderNo: "MIFA-001",
|
||||
providerAmountMinor: 125050,
|
||||
providerCode: "mifapay",
|
||||
providerCurrencyCode: "PHP",
|
||||
remark: "人工补单",
|
||||
targetUserId: "10001",
|
||||
usdAmount: 10.5,
|
||||
@ -194,16 +198,90 @@ test("builds validates and normalizes coin seller recharge orders", () => {
|
||||
).toEqual({
|
||||
appCode: "lalu",
|
||||
externalOrderNo: "MIFA-001",
|
||||
providerAmountMinor: 125050,
|
||||
providerCode: "mifapay",
|
||||
providerCurrencyCode: "PHP",
|
||||
usdAmount: 10.5,
|
||||
});
|
||||
expect(validateCoinSellerRechargeOrderPayload(payload)).toBe("");
|
||||
expect(validateCoinSellerRechargeOrderPayload({ ...payload, providerCurrencyCode: "" })).toBe("请填写币种");
|
||||
expect(validateCoinSellerRechargeOrderPayload({ ...payload, providerAmountMinor: 0 })).toBe(
|
||||
"请填写大于 0 的币种金额",
|
||||
);
|
||||
expect(validateCoinSellerRechargeOrderPayload({ ...payload, coinAmount: 0 })).toBe("");
|
||||
expect(validateCoinSellerRechargeOrderPayload({ ...payload, coinAmount: -1 })).toBe("请填写不小于 0 的整数金币");
|
||||
expect(validateCoinSellerRechargeOrderPayload({ ...payload, remark: "x".repeat(513) })).toBe("备注不能超过 512 字");
|
||||
expect(validateCoinSellerRechargeOrderPayload({ ...payload, providerCode: "usdt", chain: "" })).toBe(
|
||||
"请选择 USDT 链",
|
||||
);
|
||||
const boundaryPayload = buildCoinSellerRechargeOrderPayload({
|
||||
...payload,
|
||||
externalOrderNo: "V5-001",
|
||||
providerAmount: "0.01",
|
||||
providerCode: "v5pay",
|
||||
providerCurrencyCode: "try",
|
||||
});
|
||||
expect(boundaryPayload).toMatchObject({
|
||||
externalOrderNo: "V5-001",
|
||||
providerAmountMinor: 1,
|
||||
providerCode: "v5pay",
|
||||
providerCurrencyCode: "TRY",
|
||||
});
|
||||
expect(validateCoinSellerRechargeOrderPayload(boundaryPayload)).toBe("");
|
||||
expect(
|
||||
validateCoinSellerRechargeOrderPayload(
|
||||
buildCoinSellerRechargeOrderPayload({
|
||||
...payload,
|
||||
providerAmount: "abc",
|
||||
providerCode: "v5pay",
|
||||
providerCurrencyCode: "TRY",
|
||||
}),
|
||||
),
|
||||
).toBe("请填写大于 0 的币种金额");
|
||||
expect(
|
||||
validateCoinSellerRechargeOrderPayload(
|
||||
buildCoinSellerRechargeOrderPayload({
|
||||
...payload,
|
||||
providerAmount: "",
|
||||
providerAmountMinor: undefined,
|
||||
providerCode: "v5pay",
|
||||
providerCurrencyCode: "TRY",
|
||||
}),
|
||||
),
|
||||
).toBe("请填写大于 0 的币种金额");
|
||||
expect(
|
||||
buildCoinSellerRechargeOrderPayload({
|
||||
...payload,
|
||||
chain: "tron",
|
||||
providerAmount: "48",
|
||||
providerCode: "usdt",
|
||||
providerCurrencyCode: "PHP",
|
||||
}),
|
||||
).toEqual({
|
||||
appCode: "lalu",
|
||||
chain: "TRON",
|
||||
coinAmount: 100000,
|
||||
externalOrderNo: "MIFA-001",
|
||||
providerCode: "usdt",
|
||||
remark: "人工补单",
|
||||
targetUserId: "10001",
|
||||
usdAmount: 10.5,
|
||||
});
|
||||
expect(
|
||||
buildCoinSellerRechargeReceiptPayload({
|
||||
...payload,
|
||||
chain: "bsc",
|
||||
providerAmount: "48",
|
||||
providerCode: "usdt",
|
||||
providerCurrencyCode: "PHP",
|
||||
}),
|
||||
).toEqual({
|
||||
appCode: "lalu",
|
||||
chain: "BSC",
|
||||
externalOrderNo: "MIFA-001",
|
||||
providerCode: "usdt",
|
||||
usdAmount: 10.5,
|
||||
});
|
||||
|
||||
const page = normalizeCoinSellerRechargeOrderPage({
|
||||
items: [
|
||||
|
||||
@ -134,6 +134,7 @@ export const fallbackNavigation = [
|
||||
routeNavItem("host-agency-policy", { icon: WalletOutlined }),
|
||||
routeNavItem("host-salary-settlement", { icon: ReceiptLongOutlined }),
|
||||
routeNavItem("host-org-coin-sellers", { icon: WalletOutlined }),
|
||||
routeNavItem("host-org-coin-seller-sub-applications", { icon: ReceiptLongOutlined }),
|
||||
routeNavItem("host-withdrawals", { icon: ReceiptLongOutlined }),
|
||||
],
|
||||
},
|
||||
|
||||
@ -43,6 +43,8 @@ export const PERMISSIONS = {
|
||||
coinSellerUpdate: "coin-seller:update",
|
||||
coinSellerStockCredit: "coin-seller:stock-credit",
|
||||
coinSellerExchangeRate: "coin-seller:exchange-rate",
|
||||
coinSellerSubApplicationView: "coin-seller-sub-application:view",
|
||||
coinSellerSubApplicationAudit: "coin-seller-sub-application:audit",
|
||||
hostWithdrawalView: "host-withdrawal:view",
|
||||
financeView: "finance:view",
|
||||
financeApplicationCreate: "finance-application:create",
|
||||
@ -286,6 +288,7 @@ export const MENU_CODES = {
|
||||
hostAgencyPolicy: "host-agency-policy",
|
||||
hostSalarySettlement: "host-salary-settlement",
|
||||
hostOrgCoinSellers: "host-org-coin-sellers",
|
||||
hostOrgCoinSellerSubApplications: "host-org-coin-seller-sub-applications",
|
||||
hostWithdrawals: "host-withdrawals",
|
||||
payment: "payment",
|
||||
paymentBillList: "payment-bill-list",
|
||||
|
||||
@ -13,6 +13,8 @@ import {
|
||||
disableRegion,
|
||||
listAgencies,
|
||||
listBDs,
|
||||
listCoinSellerSubApplications,
|
||||
listCoinSellerSubSellers,
|
||||
listCoinSellers,
|
||||
listCountries,
|
||||
listHosts,
|
||||
@ -21,6 +23,8 @@ import {
|
||||
getJob,
|
||||
renameCountryCode,
|
||||
replaceRegionCountries,
|
||||
approveCoinSellerSubApplication,
|
||||
rejectCoinSellerSubApplication,
|
||||
setCoinSellerStatus,
|
||||
updateBDLeaderPositionAlias,
|
||||
updateCountry,
|
||||
@ -206,6 +210,54 @@ test("host org list APIs use generated admin paths and filters", async () => {
|
||||
expect(JSON.parse(String(agencyDeleteInit?.body))).toMatchObject({ commandId: "agency-delete-test" });
|
||||
});
|
||||
|
||||
test("coin seller sub application APIs use generated admin paths", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(
|
||||
async () => new Response(JSON.stringify({ code: 0, data: { items: [], page: 1, pageSize: 10, total: 0 } })),
|
||||
),
|
||||
);
|
||||
|
||||
await listCoinSellerSubSellers("3001", { page: 1, page_size: 10 });
|
||||
await listCoinSellerSubApplications({
|
||||
keyword: "163001",
|
||||
page: 2,
|
||||
page_size: 10,
|
||||
parent_user_id: "3001",
|
||||
status: "pending",
|
||||
target_user_id: "3002",
|
||||
});
|
||||
await approveCoinSellerSubApplication("sub-app-1", {
|
||||
commandId: "approve-sub-app-test",
|
||||
reason: "ok",
|
||||
});
|
||||
await rejectCoinSellerSubApplication("sub-app-2", {
|
||||
commandId: "reject-sub-app-test",
|
||||
reason: "bad",
|
||||
});
|
||||
|
||||
const [subSellerUrl, subSellerInit] = vi.mocked(fetch).mock.calls[0];
|
||||
const [applicationUrl, applicationInit] = vi.mocked(fetch).mock.calls[1];
|
||||
const [approveUrl, approveInit] = vi.mocked(fetch).mock.calls[2];
|
||||
const [rejectUrl, rejectInit] = vi.mocked(fetch).mock.calls[3];
|
||||
|
||||
expect(String(subSellerUrl)).toContain("/api/v1/admin/coin-sellers/3001/sub-sellers?");
|
||||
expect(String(subSellerUrl)).toContain("page=1");
|
||||
expect(subSellerInit?.method).toBe("GET");
|
||||
expect(String(applicationUrl)).toContain("/api/v1/admin/coin-seller-sub-applications?");
|
||||
expect(String(applicationUrl)).toContain("keyword=163001");
|
||||
expect(String(applicationUrl)).toContain("parent_user_id=3001");
|
||||
expect(String(applicationUrl)).toContain("target_user_id=3002");
|
||||
expect(String(applicationUrl)).toContain("status=pending");
|
||||
expect(applicationInit?.method).toBe("GET");
|
||||
expect(String(approveUrl)).toContain("/api/v1/admin/coin-seller-sub-applications/sub-app-1/approve");
|
||||
expect(approveInit?.method).toBe("POST");
|
||||
expect(JSON.parse(String(approveInit?.body))).toMatchObject({ commandId: "approve-sub-app-test" });
|
||||
expect(String(rejectUrl)).toContain("/api/v1/admin/coin-seller-sub-applications/sub-app-2/reject");
|
||||
expect(rejectInit?.method).toBe("POST");
|
||||
expect(JSON.parse(String(rejectInit?.body))).toMatchObject({ commandId: "reject-sub-app-test" });
|
||||
});
|
||||
|
||||
test("country and region APIs use generated admin paths", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
|
||||
@ -15,6 +15,9 @@ import type {
|
||||
CoinSellerDto,
|
||||
CoinSellerSalaryRatesDto,
|
||||
CoinSellerSalaryRatesPayload,
|
||||
CoinSellerSubApplicationDto,
|
||||
CoinSellerSubApplicationReviewPayload,
|
||||
CoinSellerSubApplicationReviewResultDto,
|
||||
CoinSellerStockCreditDto,
|
||||
CoinSellerStockCreditPayload,
|
||||
CoinSellerStockDebitDto,
|
||||
@ -241,6 +244,28 @@ export function listCoinSellers(query: PageQuery = {}): Promise<ApiPage<CoinSell
|
||||
});
|
||||
}
|
||||
|
||||
export function listCoinSellerSubSellers(userId: EntityId, query: PageQuery = {}): Promise<ApiPage<CoinSellerDto>> {
|
||||
const endpoint = API_ENDPOINTS.listCoinSellerSubSellers;
|
||||
return apiRequest<ApiPage<CoinSellerDto>>(
|
||||
apiEndpointPath(API_OPERATIONS.listCoinSellerSubSellers, { user_id: userId }),
|
||||
{
|
||||
method: endpoint.method,
|
||||
query,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function listCoinSellerSubApplications(query: PageQuery = {}): Promise<ApiPage<CoinSellerSubApplicationDto>> {
|
||||
const endpoint = API_ENDPOINTS.listCoinSellerSubApplications;
|
||||
return apiRequest<ApiPage<CoinSellerSubApplicationDto>>(
|
||||
apiEndpointPath(API_OPERATIONS.listCoinSellerSubApplications),
|
||||
{
|
||||
method: endpoint.method,
|
||||
query,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function listHostWithdrawals(query: PageQuery = {}): Promise<ApiPage<HostWithdrawalDto>> {
|
||||
const endpoint = API_ENDPOINTS.listHostWithdrawals;
|
||||
return apiRequest<ApiPage<HostWithdrawalDto>>(apiEndpointPath(API_OPERATIONS.listHostWithdrawals), {
|
||||
@ -339,6 +364,34 @@ export function setCoinSellerStatus(userId: EntityId, payload: CoinSellerStatusP
|
||||
);
|
||||
}
|
||||
|
||||
export function approveCoinSellerSubApplication(
|
||||
applicationId: EntityId,
|
||||
payload: CoinSellerSubApplicationReviewPayload,
|
||||
): Promise<CoinSellerSubApplicationReviewResultDto> {
|
||||
const endpoint = API_ENDPOINTS.approveCoinSellerSubApplication;
|
||||
return apiRequest<CoinSellerSubApplicationReviewResultDto, CoinSellerSubApplicationReviewPayload>(
|
||||
apiEndpointPath(API_OPERATIONS.approveCoinSellerSubApplication, { application_id: applicationId }),
|
||||
{
|
||||
body: payload,
|
||||
method: endpoint.method,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function rejectCoinSellerSubApplication(
|
||||
applicationId: EntityId,
|
||||
payload: CoinSellerSubApplicationReviewPayload,
|
||||
): Promise<CoinSellerSubApplicationReviewResultDto> {
|
||||
const endpoint = API_ENDPOINTS.rejectCoinSellerSubApplication;
|
||||
return apiRequest<CoinSellerSubApplicationReviewResultDto, CoinSellerSubApplicationReviewPayload>(
|
||||
apiEndpointPath(API_OPERATIONS.rejectCoinSellerSubApplication, { application_id: applicationId }),
|
||||
{
|
||||
body: payload,
|
||||
method: endpoint.method,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function creditCoinSellerStock(
|
||||
userId: EntityId,
|
||||
payload: CoinSellerStockCreditPayload,
|
||||
|
||||
@ -13,10 +13,13 @@ export function HostOrgStatus({ value }) {
|
||||
}
|
||||
|
||||
function getStatusTone(status) {
|
||||
if (status === "active") {
|
||||
if (status === "active" || status === "approved") {
|
||||
return "running";
|
||||
}
|
||||
if (status === "closed" || status === "disabled") {
|
||||
if (status === "pending") {
|
||||
return "warning";
|
||||
}
|
||||
if (status === "closed" || status === "disabled" || status === "rejected") {
|
||||
return "danger";
|
||||
}
|
||||
return "stopped";
|
||||
|
||||
@ -1,53 +1,69 @@
|
||||
export const agencyStatusFilters = [
|
||||
["", "全部"],
|
||||
["active", "启用"],
|
||||
["closed", "已关闭"]
|
||||
["closed", "已关闭"],
|
||||
];
|
||||
|
||||
export const countryEnabledFilters = [
|
||||
["", "全部"],
|
||||
["enabled", "启用"],
|
||||
["disabled", "停用"]
|
||||
["disabled", "停用"],
|
||||
];
|
||||
|
||||
export const regionStatusFilters = [
|
||||
["", "全部"],
|
||||
["active", "启用"],
|
||||
["disabled", "停用"]
|
||||
["disabled", "停用"],
|
||||
];
|
||||
|
||||
export const bdStatusFilters = [
|
||||
["", "全部"],
|
||||
["active", "启用"],
|
||||
["disabled", "停用"]
|
||||
["disabled", "停用"],
|
||||
];
|
||||
|
||||
export const managerStatusFilters = [
|
||||
["", "全部"],
|
||||
["active", "启用"],
|
||||
["disabled", "停用"]
|
||||
["disabled", "停用"],
|
||||
];
|
||||
|
||||
export const hostStatusFilters = [
|
||||
["", "全部"],
|
||||
["active", "启用"],
|
||||
["disabled", "停用"]
|
||||
["disabled", "停用"],
|
||||
];
|
||||
|
||||
export const coinSellerStatusFilters = [
|
||||
["", "全部"],
|
||||
["active", "启用"],
|
||||
["disabled", "停用"]
|
||||
["disabled", "停用"],
|
||||
];
|
||||
|
||||
export const coinSellerSubApplicationStatusFilters = [
|
||||
["", "全部"],
|
||||
["pending", "待审核"],
|
||||
["approved", "已通过"],
|
||||
["rejected", "已拒绝"],
|
||||
];
|
||||
|
||||
export const coinSellerSubApplicationStatusLabels = {
|
||||
approved: "已通过",
|
||||
pending: "待审核",
|
||||
rejected: "已拒绝",
|
||||
};
|
||||
|
||||
export const hostSourceLabels = {
|
||||
agency_application: "Agency 申请",
|
||||
agency_invitation: "Agency 邀请",
|
||||
admin_create_agency: "后台创建 Agency"
|
||||
admin_create_agency: "后台创建 Agency",
|
||||
};
|
||||
|
||||
export const statusLabels = {
|
||||
active: "启用",
|
||||
approved: "已通过",
|
||||
closed: "已关闭",
|
||||
disabled: "停用"
|
||||
disabled: "停用",
|
||||
pending: "待审核",
|
||||
rejected: "已拒绝",
|
||||
};
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
debitCoinSellerStock,
|
||||
getCoinSellerSalaryRates,
|
||||
listCoinSellers,
|
||||
listCoinSellerSubSellers,
|
||||
replaceCoinSellerSalaryRates,
|
||||
setCoinSellerStatus,
|
||||
} from "@/features/host-org/api";
|
||||
@ -79,6 +80,9 @@ export function useHostCoinSellersPage() {
|
||||
const [loadingRates, setLoadingRates] = useState(false);
|
||||
const [selectedSeller, setSelectedSeller] = useState(null);
|
||||
const [loadingAction, setLoadingAction] = useState("");
|
||||
const [loadingSubSellers, setLoadingSubSellers] = useState(false);
|
||||
const [subSellerData, setSubSellerData] = useState(emptyData);
|
||||
const [subSellerPage, setSubSellerPageState] = useState(1);
|
||||
const [contactSavingIds, setContactSavingIds] = useState({});
|
||||
const [sellerRowPatches, setSellerRowPatches] = useState({});
|
||||
const filters = useMemo(
|
||||
@ -193,6 +197,15 @@ export function useHostCoinSellersPage() {
|
||||
setActiveAction("ledger");
|
||||
};
|
||||
|
||||
const openSubSellerList = async (seller) => {
|
||||
if (!seller?.userId || !abilities.canView) {
|
||||
return;
|
||||
}
|
||||
setSelectedSeller(seller);
|
||||
setActiveAction("sub-sellers");
|
||||
await loadSubSellers(seller.userId, 1);
|
||||
};
|
||||
|
||||
const openRateSettings = async () => {
|
||||
if (!abilities.canExchangeRate) {
|
||||
return;
|
||||
@ -212,6 +225,31 @@ export function useHostCoinSellersPage() {
|
||||
setSelectedSeller(null);
|
||||
};
|
||||
|
||||
const loadSubSellers = async (sellerUserId = selectedSeller?.userId, nextPage = subSellerPage) => {
|
||||
if (!sellerUserId) {
|
||||
setSubSellerData(emptyData);
|
||||
return;
|
||||
}
|
||||
setLoadingSubSellers(true);
|
||||
try {
|
||||
const nextData = await listCoinSellerSubSellers(sellerUserId, {
|
||||
page: nextPage,
|
||||
page_size: pageSize,
|
||||
});
|
||||
setSubSellerData(nextData || emptyData);
|
||||
setSubSellerPageState(nextPage);
|
||||
} catch (err) {
|
||||
setSubSellerData(emptyData);
|
||||
showToast(err.message || "加载子币商列表失败", "error");
|
||||
} finally {
|
||||
setLoadingSubSellers(false);
|
||||
}
|
||||
};
|
||||
|
||||
const setSubSellerPage = async (nextPage) => {
|
||||
await loadSubSellers(selectedSeller?.userId, nextPage);
|
||||
};
|
||||
|
||||
const changeRateRegionId = async (value) => {
|
||||
setRateRegionId(value);
|
||||
if (value) {
|
||||
@ -453,6 +491,7 @@ export function useHostCoinSellersPage() {
|
||||
error,
|
||||
loading,
|
||||
loadingAction,
|
||||
loadingSubSellers,
|
||||
loadingRates,
|
||||
loadingRegions,
|
||||
openRateSettings,
|
||||
@ -461,6 +500,7 @@ export function useHostCoinSellersPage() {
|
||||
openSellerLedger,
|
||||
openStockCredit,
|
||||
openStockDebit,
|
||||
openSubSellerList,
|
||||
page,
|
||||
regionId,
|
||||
regionOptions,
|
||||
@ -478,9 +518,12 @@ export function useHostCoinSellersPage() {
|
||||
setPage,
|
||||
setStockForm,
|
||||
setStockDebitForm,
|
||||
setSubSellerPage,
|
||||
status,
|
||||
stockDebitForm,
|
||||
stockForm,
|
||||
subSellerData,
|
||||
subSellerPage,
|
||||
sortBy,
|
||||
sortDirection,
|
||||
submitRateSettings,
|
||||
|
||||
@ -0,0 +1,152 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { parseForm } from "@/shared/forms/validation";
|
||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||
import {
|
||||
approveCoinSellerSubApplication,
|
||||
listCoinSellerSubApplications,
|
||||
rejectCoinSellerSubApplication,
|
||||
} from "@/features/host-org/api";
|
||||
import { useCoinSellerSubApplicationAbilities } from "@/features/host-org/permissions.js";
|
||||
import { coinSellerSubApplicationReviewSchema } from "@/features/host-org/schema";
|
||||
|
||||
const pageSize = 50;
|
||||
const emptyData = { items: [], page: 1, pageSize, total: 0 };
|
||||
|
||||
const emptyReviewForm = (decision = "approved") => ({
|
||||
commandId: makeCommandId(`coin-seller-sub-${decision}`),
|
||||
reason: "",
|
||||
});
|
||||
|
||||
export function useHostSubCoinSellerApplicationsPage() {
|
||||
const abilities = useCoinSellerSubApplicationAbilities();
|
||||
const { showToast } = useToast();
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [status, setStatus] = useState("pending");
|
||||
const [parentUserId, setParentUserId] = useState("");
|
||||
const [targetUserId, setTargetUserId] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const [activeAction, setActiveAction] = useState("");
|
||||
const [selectedApplication, setSelectedApplication] = useState(null);
|
||||
const [reviewDecision, setReviewDecision] = useState("approved");
|
||||
const [reviewForm, setReviewForm] = useState(emptyReviewForm);
|
||||
const [loadingAction, setLoadingAction] = useState("");
|
||||
const filters = useMemo(
|
||||
() => ({
|
||||
keyword,
|
||||
parent_user_id: parentUserId,
|
||||
status,
|
||||
target_user_id: targetUserId,
|
||||
}),
|
||||
[keyword, parentUserId, status, targetUserId],
|
||||
);
|
||||
const {
|
||||
data = emptyData,
|
||||
error,
|
||||
loading,
|
||||
reload,
|
||||
} = usePaginatedQuery({
|
||||
errorMessage: "加载子币商申请列表失败",
|
||||
fetcher: listCoinSellerSubApplications,
|
||||
filters,
|
||||
page,
|
||||
pageSize,
|
||||
queryKey: ["host-org", "coin-seller-sub-applications", filters, page],
|
||||
});
|
||||
|
||||
const changeKeyword = (value) => {
|
||||
setKeyword(value);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const changeStatus = (value) => {
|
||||
setStatus(value);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const changeParentUserId = (value) => {
|
||||
setParentUserId(String(value || "").trim());
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const changeTargetUserId = (value) => {
|
||||
setTargetUserId(String(value || "").trim());
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const resetFilters = () => {
|
||||
setKeyword("");
|
||||
setStatus("");
|
||||
setParentUserId("");
|
||||
setTargetUserId("");
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const openReview = (application, decision) => {
|
||||
if (!application?.applicationId || !abilities.canAudit || application.status !== "pending") {
|
||||
return;
|
||||
}
|
||||
// 审核 commandId 与列表刷新分离,避免弹窗重复提交时把另一次审核误认为同一请求。
|
||||
setSelectedApplication(application);
|
||||
setReviewDecision(decision);
|
||||
setReviewForm(emptyReviewForm(decision));
|
||||
setActiveAction("review");
|
||||
};
|
||||
|
||||
const closeAction = () => {
|
||||
setActiveAction("");
|
||||
setSelectedApplication(null);
|
||||
};
|
||||
|
||||
const submitReview = async (event) => {
|
||||
event.preventDefault();
|
||||
if (!selectedApplication?.applicationId || !abilities.canAudit) {
|
||||
return;
|
||||
}
|
||||
const payload = parseForm(coinSellerSubApplicationReviewSchema, reviewForm);
|
||||
const submitter =
|
||||
reviewDecision === "approved" ? approveCoinSellerSubApplication : rejectCoinSellerSubApplication;
|
||||
setLoadingAction(`review-${selectedApplication.applicationId}`);
|
||||
try {
|
||||
await submitter(selectedApplication.applicationId, payload);
|
||||
showToast(reviewDecision === "approved" ? "子币商申请已通过" : "子币商申请已拒绝", "success");
|
||||
closeAction();
|
||||
await reload();
|
||||
} catch (err) {
|
||||
showToast(err.message || "审核子币商申请失败", "error");
|
||||
} finally {
|
||||
setLoadingAction("");
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
abilities,
|
||||
activeAction,
|
||||
changeKeyword,
|
||||
changeParentUserId,
|
||||
changeStatus,
|
||||
changeTargetUserId,
|
||||
closeAction,
|
||||
data,
|
||||
error,
|
||||
keyword,
|
||||
loading,
|
||||
loadingAction,
|
||||
openReview,
|
||||
page,
|
||||
parentUserId,
|
||||
resetFilters,
|
||||
reviewDecision,
|
||||
reviewForm,
|
||||
selectedApplication,
|
||||
setPage,
|
||||
setReviewForm,
|
||||
status,
|
||||
submitReview,
|
||||
targetUserId,
|
||||
};
|
||||
}
|
||||
|
||||
function makeCommandId(prefix) {
|
||||
return `${prefix}-${Date.now()}`;
|
||||
}
|
||||
@ -78,6 +78,39 @@ const baseColumns = [
|
||||
},
|
||||
];
|
||||
|
||||
const subSellerColumns = [
|
||||
{
|
||||
key: "seller",
|
||||
label: "子币商",
|
||||
render: (item) => <SellerIdentity item={item} />,
|
||||
width: "minmax(220px, 1.3fr)",
|
||||
},
|
||||
{
|
||||
key: "regionId",
|
||||
label: "区域",
|
||||
render: (item, _index, context) => regionName(item, context?.regionOptions),
|
||||
width: "minmax(140px, 0.8fr)",
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
label: "状态",
|
||||
render: (item) => item.status || "-",
|
||||
width: "minmax(90px, 0.5fr)",
|
||||
},
|
||||
{
|
||||
key: "merchantBalance",
|
||||
label: "币商余额",
|
||||
render: (item) => formatNumber(item.merchantBalance),
|
||||
width: "minmax(120px, 0.8fr)",
|
||||
},
|
||||
{
|
||||
key: "updatedAtMs",
|
||||
label: "更新时间",
|
||||
render: (item) => formatMillis(item.updatedAtMs),
|
||||
width: "minmax(170px, 1fr)",
|
||||
},
|
||||
];
|
||||
|
||||
export function HostCoinSellersPage() {
|
||||
const page = useHostCoinSellersPage();
|
||||
const items = page.data.items || [];
|
||||
@ -99,6 +132,12 @@ export function HostCoinSellersPage() {
|
||||
render: (item) => <SubCoinSellerPermissionSwitch item={item} page={page} />,
|
||||
width: "minmax(120px, 0.7fr)",
|
||||
},
|
||||
{
|
||||
key: "subCoinSellerCount",
|
||||
label: "子币商数量",
|
||||
render: (item) => <SubCoinSellerCountButton item={item} page={page} />,
|
||||
width: "minmax(120px, 0.7fr)",
|
||||
},
|
||||
...baseColumns.slice(2),
|
||||
page.abilities.canUpdate || page.abilities.canStockCredit || page.abilities.canLedger
|
||||
? {
|
||||
@ -220,9 +259,7 @@ export function HostCoinSellersPage() {
|
||||
checked={page.createForm.canManageSubCoinSellers !== false}
|
||||
disabled={createDisabled}
|
||||
label="子币商权限"
|
||||
onChange={(checked) =>
|
||||
page.setCreateForm({ ...page.createForm, canManageSubCoinSellers: checked })
|
||||
}
|
||||
onChange={(checked) => page.setCreateForm({ ...page.createForm, canManageSubCoinSellers: checked })}
|
||||
/>
|
||||
</HostOrgActionModal>
|
||||
|
||||
@ -380,7 +417,9 @@ export function HostCoinSellersPage() {
|
||||
required
|
||||
minRows={2}
|
||||
value={stockDebitForm.reason}
|
||||
onChange={(event) => page.setStockDebitForm?.({ ...stockDebitForm, reason: event.target.value })}
|
||||
onChange={(event) =>
|
||||
page.setStockDebitForm?.({ ...stockDebitForm, reason: event.target.value })
|
||||
}
|
||||
/>
|
||||
</AdminFormSection>
|
||||
</HostOrgActionModal>
|
||||
@ -401,6 +440,35 @@ export function HostCoinSellersPage() {
|
||||
) : null}
|
||||
</SideDrawer>
|
||||
|
||||
<SideDrawer
|
||||
className={styles.ledgerDrawer}
|
||||
contentClassName={styles.ledgerDrawerBody}
|
||||
open={page.activeAction === "sub-sellers"}
|
||||
title={`子币商列表 - ${sellerDrawerTitle(page.selectedSeller)}`}
|
||||
width="wide"
|
||||
onClose={page.closeAction}
|
||||
>
|
||||
<DataState loading={page.loadingSubSellers}>
|
||||
<HostOrgTable
|
||||
columns={subSellerColumns}
|
||||
context={{ regionOptions: page.regionOptions }}
|
||||
items={page.subSellerData.items || []}
|
||||
minWidth="860px"
|
||||
pagination={
|
||||
Number(page.subSellerData.total || 0) > 0
|
||||
? {
|
||||
page: page.subSellerPage,
|
||||
pageSize: page.subSellerData.pageSize || 50,
|
||||
total: page.subSellerData.total,
|
||||
onPageChange: page.setSubSellerPage,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
rowKey={(item) => item.userId}
|
||||
/>
|
||||
</DataState>
|
||||
</SideDrawer>
|
||||
|
||||
<SideDrawer
|
||||
actions={
|
||||
<>
|
||||
@ -509,9 +577,7 @@ function SubCoinSellerPermissionSwitch({ item, page }) {
|
||||
<AdminSwitch
|
||||
checked={item.canManageSubCoinSellers === true}
|
||||
checkedLabel="开"
|
||||
disabled={
|
||||
!page.abilities.canUpdate || page.loadingAction === `coin-seller-sub-permission-${item.userId}`
|
||||
}
|
||||
disabled={!page.abilities.canUpdate || page.loadingAction === `coin-seller-sub-permission-${item.userId}`}
|
||||
inputProps={{ "aria-label": item.canManageSubCoinSellers === true ? "关闭子币商权限" : "开启子币商权限" }}
|
||||
uncheckedLabel="关"
|
||||
onChange={(event) => page.toggleSubCoinSellerPermission(item, event.target.checked)}
|
||||
@ -519,6 +585,20 @@ function SubCoinSellerPermissionSwitch({ item, page }) {
|
||||
);
|
||||
}
|
||||
|
||||
function SubCoinSellerCountButton({ item, page }) {
|
||||
const count = Number(item.subCoinSellerCount || 0);
|
||||
return (
|
||||
<Button
|
||||
disabled={!page.abilities.canView || page.loadingSubSellers}
|
||||
size="small"
|
||||
variant="text"
|
||||
onClick={() => page.openSubSellerList(item)}
|
||||
>
|
||||
{formatNumber(count)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function SellerContact({ item, page }) {
|
||||
const contact = String(item.contact || "").trim();
|
||||
return (
|
||||
|
||||
@ -6,6 +6,7 @@ import { useHostCoinSellersPage } from "@/features/host-org/hooks/useHostCoinSel
|
||||
const mocks = vi.hoisted(() => ({
|
||||
ledgerTable: vi.fn(),
|
||||
openSellerLedger: vi.fn(),
|
||||
openSubSellerList: vi.fn(),
|
||||
toggleSubCoinSellerPermission: vi.fn(),
|
||||
}));
|
||||
|
||||
@ -43,9 +44,25 @@ test("coin seller table shows cumulative USDT and hides creator id column", () =
|
||||
expect(screen.queryByText("创建人 ID")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("累充USDT")).toBeInTheDocument();
|
||||
expect(screen.getByText("子币商权限")).toBeInTheDocument();
|
||||
expect(screen.getByText("子币商数量")).toBeInTheDocument();
|
||||
expect(screen.getByText("12.5")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("coin seller sub seller count opens direct child seller drawer", () => {
|
||||
const seller = sellerFixture({ subCoinSellerCount: 2 });
|
||||
vi.mocked(useHostCoinSellersPage).mockReturnValue(
|
||||
pageFixture({
|
||||
data: { items: [seller], page: 1, pageSize: 50, total: 1 },
|
||||
}),
|
||||
);
|
||||
|
||||
render(<HostCoinSellersPage />);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "2" }));
|
||||
|
||||
expect(mocks.openSubSellerList).toHaveBeenCalledWith(seller);
|
||||
});
|
||||
|
||||
test("coin seller sub permission switch toggles through the page hook", () => {
|
||||
const seller = sellerFixture();
|
||||
vi.mocked(useHostCoinSellersPage).mockReturnValue(
|
||||
@ -104,7 +121,34 @@ test("coin seller ledger drawer passes locked seller id to shared table", () =>
|
||||
render(<HostCoinSellersPage />);
|
||||
|
||||
expect(screen.getByTestId("coin-seller-ledger-table")).toHaveTextContent("3001");
|
||||
expect(mocks.ledgerTable).toHaveBeenCalledWith(expect.objectContaining({ lockedSeller: seller, sellerUserId: "3001" }));
|
||||
expect(mocks.ledgerTable).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ lockedSeller: seller, sellerUserId: "3001" }),
|
||||
);
|
||||
});
|
||||
|
||||
test("coin seller sub seller drawer renders approved direct children", () => {
|
||||
const seller = sellerFixture({ subCoinSellerCount: 1 });
|
||||
const child = sellerFixture({
|
||||
displayUserId: "168933",
|
||||
merchantBalance: 1200,
|
||||
subCoinSellerCount: 0,
|
||||
userId: "3002",
|
||||
username: "Child seller",
|
||||
});
|
||||
vi.mocked(useHostCoinSellersPage).mockReturnValue(
|
||||
pageFixture({
|
||||
activeAction: "sub-sellers",
|
||||
data: { items: [seller], page: 1, pageSize: 50, total: 1 },
|
||||
selectedSeller: seller,
|
||||
subSellerData: { items: [child], page: 1, pageSize: 50, total: 1 },
|
||||
}),
|
||||
);
|
||||
|
||||
render(<HostCoinSellersPage />);
|
||||
|
||||
expect(screen.getByText("子币商列表 - Flower92 / 164425")).toBeInTheDocument();
|
||||
expect(screen.getByText("Child seller")).toBeInTheDocument();
|
||||
expect(screen.getByText("168933")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("coin seller create form includes sub permission switch", () => {
|
||||
@ -189,6 +233,7 @@ function pageFixture(patch = {}) {
|
||||
openRateSettings: vi.fn(),
|
||||
openSellerLedger: mocks.openSellerLedger,
|
||||
openStockCredit: vi.fn(),
|
||||
openSubSellerList: mocks.openSubSellerList,
|
||||
page: 1,
|
||||
query: "",
|
||||
rateRegionId: "",
|
||||
@ -203,8 +248,11 @@ function pageFixture(patch = {}) {
|
||||
setCreateForm: vi.fn(),
|
||||
setEditForm: vi.fn(),
|
||||
setPage: vi.fn(),
|
||||
setSubSellerPage: vi.fn(),
|
||||
setStockForm: vi.fn(),
|
||||
status: "",
|
||||
subSellerData: { items: [], page: 1, pageSize: 50, total: 0 },
|
||||
subSellerPage: 1,
|
||||
stockDebitForm: { coinAmount: "", reason: "", rechargeAmount: "" },
|
||||
stockForm: { coinAmount: "", reason: "", rechargeAmount: "", type: "usdt_purchase" },
|
||||
sortBy: "",
|
||||
@ -220,7 +268,7 @@ function pageFixture(patch = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function sellerFixture() {
|
||||
function sellerFixture(patch = {}) {
|
||||
return {
|
||||
avatar: "",
|
||||
canManageSubCoinSellers: true,
|
||||
@ -230,9 +278,11 @@ function sellerFixture() {
|
||||
merchantBalance: 9400000,
|
||||
regionId: 1,
|
||||
status: "active",
|
||||
subCoinSellerCount: 2,
|
||||
totalRechargeUsdtMicro: 12500000,
|
||||
updatedAtMs: 1760000000000,
|
||||
userId: "3001",
|
||||
username: "Flower92",
|
||||
...patch,
|
||||
};
|
||||
}
|
||||
|
||||
@ -0,0 +1,205 @@
|
||||
import CheckCircleOutlineOutlined from "@mui/icons-material/CheckCircleOutlineOutlined";
|
||||
import HighlightOffOutlined from "@mui/icons-material/HighlightOffOutlined";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { AdminActionIconButton, AdminRowActions } from "@/shared/ui/AdminListLayout.jsx";
|
||||
import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
|
||||
import { Button } from "@/shared/ui/Button.jsx";
|
||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||
import { formatMillis } from "@/shared/utils/time.js";
|
||||
import {
|
||||
coinSellerSubApplicationStatusFilters,
|
||||
coinSellerSubApplicationStatusLabels,
|
||||
} from "@/features/host-org/constants.js";
|
||||
import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx";
|
||||
import { HostOrgStatus } from "@/features/host-org/components/HostOrgStatus.jsx";
|
||||
import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
|
||||
import { useHostSubCoinSellerApplicationsPage } from "@/features/host-org/hooks/useHostSubCoinSellerApplicationsPage.js";
|
||||
import styles from "@/features/host-org/host-org.module.css";
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: "parent",
|
||||
label: "父币商",
|
||||
render: (item) => <ApplicationUserIdentity user={item.parent} />,
|
||||
width: "minmax(220px, 1.2fr)",
|
||||
},
|
||||
{
|
||||
key: "target",
|
||||
label: "申请子币商",
|
||||
render: (item) => <ApplicationUserIdentity user={item.target} />,
|
||||
width: "minmax(220px, 1.2fr)",
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
label: "状态",
|
||||
render: (item) => <HostOrgStatus value={item.status} />,
|
||||
width: "minmax(120px, 0.6fr)",
|
||||
},
|
||||
{
|
||||
key: "createdAtMs",
|
||||
label: "申请时间",
|
||||
render: (item) => formatMillis(item.createdAtMs),
|
||||
width: "minmax(170px, 0.9fr)",
|
||||
},
|
||||
{
|
||||
key: "reviewedAtMs",
|
||||
label: "审核时间",
|
||||
render: (item) => (Number(item.reviewedAtMs || 0) > 0 ? formatMillis(item.reviewedAtMs) : "-"),
|
||||
width: "minmax(170px, 0.9fr)",
|
||||
},
|
||||
{
|
||||
key: "reviewedByAdminId",
|
||||
label: "审核人",
|
||||
render: (item) => item.reviewedByAdminId || "-",
|
||||
width: "minmax(120px, 0.6fr)",
|
||||
},
|
||||
{
|
||||
key: "reviewReason",
|
||||
label: "审核备注",
|
||||
render: (item) => item.reviewReason || "-",
|
||||
width: "minmax(180px, 1fr)",
|
||||
},
|
||||
{
|
||||
key: "actions",
|
||||
label: "操作",
|
||||
render: (item, _index, context) => <ApplicationActions item={item} page={context?.page} />,
|
||||
width: "minmax(120px, 0.7fr)",
|
||||
},
|
||||
];
|
||||
|
||||
export function HostSubCoinSellerApplicationsPage() {
|
||||
const page = useHostSubCoinSellerApplicationsPage();
|
||||
const items = page.data.items || [];
|
||||
const total = page.data.total || 0;
|
||||
const reviewTitle = page.reviewDecision === "approved" ? "通过子币商申请" : "拒绝子币商申请";
|
||||
|
||||
return (
|
||||
<section className={styles.root}>
|
||||
<div className={styles.contentPanel}>
|
||||
<div className={styles.toolbar}>
|
||||
<section className={styles.filters}>
|
||||
<TextField
|
||||
label="搜索"
|
||||
size="small"
|
||||
value={page.keyword}
|
||||
onChange={(event) => page.changeKeyword(event.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
label="状态"
|
||||
select
|
||||
size="small"
|
||||
value={page.status}
|
||||
onChange={(event) => page.changeStatus(event.target.value)}
|
||||
>
|
||||
{coinSellerSubApplicationStatusFilters.map(([value, label]) => (
|
||||
<MenuItem key={value || "all"} value={value}>
|
||||
{label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<TextField
|
||||
label="父币商ID"
|
||||
size="small"
|
||||
type="number"
|
||||
value={page.parentUserId}
|
||||
onChange={(event) => page.changeParentUserId(event.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
label="目标用户ID"
|
||||
size="small"
|
||||
type="number"
|
||||
value={page.targetUserId}
|
||||
onChange={(event) => page.changeTargetUserId(event.target.value)}
|
||||
/>
|
||||
<Button variant="text" onClick={page.resetFilters}>
|
||||
重置
|
||||
</Button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<DataState error={page.error} loading={page.loading}>
|
||||
<div className={styles.listBlock}>
|
||||
<HostOrgTable
|
||||
columns={columns}
|
||||
context={{ page }}
|
||||
items={items}
|
||||
minWidth="1280px"
|
||||
pagination={
|
||||
total > 0
|
||||
? {
|
||||
page: page.page,
|
||||
pageSize: page.data.pageSize || 50,
|
||||
total,
|
||||
onPageChange: page.setPage,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
rowKey={(item) => item.applicationId}
|
||||
/>
|
||||
</div>
|
||||
</DataState>
|
||||
</div>
|
||||
|
||||
<HostOrgActionModal
|
||||
disabled={!page.abilities.canAudit}
|
||||
loading={String(page.loadingAction).startsWith("review-")}
|
||||
open={page.activeAction === "review"}
|
||||
sectionTitle="审核信息"
|
||||
submitLabel={page.reviewDecision === "approved" ? "确认通过" : "确认拒绝"}
|
||||
onClose={page.closeAction}
|
||||
onSubmit={page.submitReview}
|
||||
title={reviewTitle}
|
||||
>
|
||||
<TextField disabled label="申请状态" value={statusText(page.selectedApplication?.status)} />
|
||||
<TextField disabled label="Command ID" value={page.reviewForm.commandId} />
|
||||
<TextField
|
||||
disabled={!page.abilities.canAudit}
|
||||
label="审核备注"
|
||||
multiline
|
||||
minRows={3}
|
||||
value={page.reviewForm.reason}
|
||||
onChange={(event) => page.setReviewForm({ ...page.reviewForm, reason: event.target.value })}
|
||||
/>
|
||||
</HostOrgActionModal>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function ApplicationUserIdentity({ user }) {
|
||||
return (
|
||||
<AdminUserIdentity
|
||||
avatar={user?.avatar}
|
||||
displayUserId={user?.displayUserId}
|
||||
name={user?.username}
|
||||
rows={[user?.userId, user?.regionName].filter(Boolean)}
|
||||
userId={user?.userId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ApplicationActions({ item, page }) {
|
||||
if (!page?.abilities.canAudit || item.status !== "pending") {
|
||||
return <span>-</span>;
|
||||
}
|
||||
const loading = page.loadingAction === `review-${item.applicationId}`;
|
||||
return (
|
||||
<AdminRowActions>
|
||||
<AdminActionIconButton
|
||||
disabled={loading}
|
||||
label="通过"
|
||||
primary
|
||||
onClick={() => page.openReview(item, "approved")}
|
||||
>
|
||||
<CheckCircleOutlineOutlined fontSize="small" />
|
||||
</AdminActionIconButton>
|
||||
<AdminActionIconButton disabled={loading} label="拒绝" onClick={() => page.openReview(item, "rejected")}>
|
||||
<HighlightOffOutlined fontSize="small" />
|
||||
</AdminActionIconButton>
|
||||
</AdminRowActions>
|
||||
);
|
||||
}
|
||||
|
||||
function statusText(status) {
|
||||
return coinSellerSubApplicationStatusLabels[status] || status || "-";
|
||||
}
|
||||
@ -0,0 +1,110 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { HostSubCoinSellerApplicationsPage } from "./HostSubCoinSellerApplicationsPage.jsx";
|
||||
import { useHostSubCoinSellerApplicationsPage } from "@/features/host-org/hooks/useHostSubCoinSellerApplicationsPage.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
openReview: vi.fn(),
|
||||
resetFilters: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/features/host-org/hooks/useHostSubCoinSellerApplicationsPage.js", () => ({
|
||||
useHostSubCoinSellerApplicationsPage: vi.fn(),
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("sub coin seller application page renders pending applications with audit actions", () => {
|
||||
const application = applicationFixture();
|
||||
vi.mocked(useHostSubCoinSellerApplicationsPage).mockReturnValue(pageFixture({ data: pageData([application]) }));
|
||||
|
||||
render(<HostSubCoinSellerApplicationsPage />);
|
||||
|
||||
expect(screen.getByText("父币商")).toBeInTheDocument();
|
||||
expect(screen.getByText("申请子币商")).toBeInTheDocument();
|
||||
expect(screen.getByText("Parent seller")).toBeInTheDocument();
|
||||
expect(screen.getByText("Target user")).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getAllByLabelText("通过").find((node) => node.tagName.toLowerCase() === "button"));
|
||||
fireEvent.click(screen.getAllByLabelText("拒绝").find((node) => node.tagName.toLowerCase() === "button"));
|
||||
|
||||
expect(mocks.openReview).toHaveBeenCalledWith(application, "approved");
|
||||
expect(mocks.openReview).toHaveBeenCalledWith(application, "rejected");
|
||||
});
|
||||
|
||||
test("sub coin seller application page hides audit actions for reviewed rows", () => {
|
||||
vi.mocked(useHostSubCoinSellerApplicationsPage).mockReturnValue(
|
||||
pageFixture({ data: pageData([applicationFixture({ status: "approved" })]) }),
|
||||
);
|
||||
|
||||
render(<HostSubCoinSellerApplicationsPage />);
|
||||
|
||||
expect(screen.queryByLabelText("通过")).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("拒绝")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
function pageFixture(patch = {}) {
|
||||
return {
|
||||
abilities: { canAudit: true, canView: true },
|
||||
activeAction: "",
|
||||
changeKeyword: vi.fn(),
|
||||
changeParentUserId: vi.fn(),
|
||||
changeStatus: vi.fn(),
|
||||
changeTargetUserId: vi.fn(),
|
||||
closeAction: vi.fn(),
|
||||
data: pageData([applicationFixture()]),
|
||||
error: null,
|
||||
keyword: "",
|
||||
loading: false,
|
||||
loadingAction: "",
|
||||
openReview: mocks.openReview,
|
||||
page: 1,
|
||||
parentUserId: "",
|
||||
resetFilters: mocks.resetFilters,
|
||||
reviewDecision: "approved",
|
||||
reviewForm: { commandId: "coin-seller-sub-approved-test", reason: "" },
|
||||
selectedApplication: null,
|
||||
setPage: vi.fn(),
|
||||
setReviewForm: vi.fn(),
|
||||
status: "pending",
|
||||
submitReview: vi.fn((event) => event.preventDefault()),
|
||||
targetUserId: "",
|
||||
...patch,
|
||||
};
|
||||
}
|
||||
|
||||
function pageData(items) {
|
||||
return { items, page: 1, pageSize: 50, total: items.length };
|
||||
}
|
||||
|
||||
function applicationFixture(patch = {}) {
|
||||
return {
|
||||
applicationId: "sub-app-1",
|
||||
createdAtMs: 1760000000000,
|
||||
parent: {
|
||||
avatar: "",
|
||||
displayUserId: "163001",
|
||||
regionName: "中东区",
|
||||
userId: "3001",
|
||||
username: "Parent seller",
|
||||
},
|
||||
parentUserId: "3001",
|
||||
relationId: "",
|
||||
reviewReason: "",
|
||||
reviewedAtMs: 0,
|
||||
reviewedByAdminId: "",
|
||||
status: "pending",
|
||||
target: {
|
||||
avatar: "",
|
||||
displayUserId: "168933",
|
||||
regionName: "中东区",
|
||||
userId: "3002",
|
||||
username: "Target user",
|
||||
},
|
||||
targetUserId: "3002",
|
||||
updatedAtMs: 1760000000000,
|
||||
...patch,
|
||||
};
|
||||
}
|
||||
@ -68,6 +68,15 @@ export function useCoinSellerAbilities() {
|
||||
};
|
||||
}
|
||||
|
||||
export function useCoinSellerSubApplicationAbilities() {
|
||||
const { can } = useAuth();
|
||||
|
||||
return {
|
||||
canAudit: can(PERMISSIONS.coinSellerSubApplicationAudit),
|
||||
canView: can(PERMISSIONS.coinSellerSubApplicationView),
|
||||
};
|
||||
}
|
||||
|
||||
export function useHostWithdrawalAbilities() {
|
||||
const { can } = useAuth();
|
||||
|
||||
|
||||
@ -65,6 +65,17 @@ export const hostOrgRoutes = [
|
||||
path: "/host/coin-sellers",
|
||||
permission: PERMISSIONS.coinSellerView,
|
||||
},
|
||||
{
|
||||
label: "Sub Coin Seller 申请列表",
|
||||
loader: () =>
|
||||
import("./pages/HostSubCoinSellerApplicationsPage.jsx").then(
|
||||
(module) => module.HostSubCoinSellerApplicationsPage,
|
||||
),
|
||||
menuCode: MENU_CODES.hostOrgCoinSellerSubApplications,
|
||||
pageKey: "host-org-coin-seller-sub-applications",
|
||||
path: "/host/coin-seller-sub-applications",
|
||||
permission: PERMISSIONS.coinSellerSubApplicationView,
|
||||
},
|
||||
{
|
||||
label: "主播提现",
|
||||
loader: () => import("./pages/HostWithdrawalsPage.jsx").then((module) => module.HostWithdrawalsPage),
|
||||
|
||||
@ -3,6 +3,7 @@ import {
|
||||
agencyCloseSchema,
|
||||
agencyJoinEnabledSchema,
|
||||
coinSellerStatusSchema,
|
||||
coinSellerSubApplicationReviewSchema,
|
||||
createCoinSellerSchema,
|
||||
createManagerSchema,
|
||||
countryRenameCodeSchema,
|
||||
@ -56,6 +57,19 @@ test("coin seller schemas preserve sub seller permission defaults and explicit f
|
||||
}
|
||||
});
|
||||
|
||||
test("coin seller sub application review schema keeps command id and reason", () => {
|
||||
const result = coinSellerSubApplicationReviewSchema.safeParse({
|
||||
commandId: "coin-seller-sub-application-review-test",
|
||||
reason: "same region and profile is active",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.commandId).toBe("coin-seller-sub-application-review-test");
|
||||
expect(result.data.reason).toBe("same region and profile is active");
|
||||
}
|
||||
});
|
||||
|
||||
test("agency action schemas preserve large agency ids as strings", () => {
|
||||
const agencyId = "321170072154411009";
|
||||
const closeResult = agencyCloseSchema.safeParse({
|
||||
|
||||
@ -130,6 +130,8 @@ export const coinSellerStatusSchema = commandContactSchema.extend({
|
||||
targetUserId: entityIdSchema,
|
||||
});
|
||||
|
||||
export const coinSellerSubApplicationReviewSchema = commandBaseSchema;
|
||||
|
||||
export const coinSellerStockCreditSchema = z
|
||||
.object({
|
||||
coinAmount: z.coerce.number().int().positive("请输入充值金币"),
|
||||
@ -219,6 +221,7 @@ export type CreateBDForm = z.infer<typeof createBDSchema>;
|
||||
export type BDStatusForm = z.infer<typeof bdStatusSchema>;
|
||||
export type CreateCoinSellerForm = z.infer<typeof createCoinSellerSchema>;
|
||||
export type CoinSellerStatusForm = z.infer<typeof coinSellerStatusSchema>;
|
||||
export type CoinSellerSubApplicationReviewForm = z.infer<typeof coinSellerSubApplicationReviewSchema>;
|
||||
export type CoinSellerStockCreditForm = z.infer<typeof coinSellerStockCreditSchema>;
|
||||
export type CoinSellerStockDebitForm = z.infer<typeof coinSellerStockDebitSchema>;
|
||||
export type CreateAgencyForm = z.infer<typeof createAgencySchema>;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
281
src/shared/api/generated/schema.d.ts
vendored
281
src/shared/api/generated/schema.d.ts
vendored
@ -1108,6 +1108,70 @@ export interface paths {
|
||||
patch: operations["setCoinSellerStatus"];
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/coin-sellers/{user_id}/sub-sellers": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["listCoinSellerSubSellers"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/coin-seller-sub-applications": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["listCoinSellerSubApplications"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/coin-seller-sub-applications/{application_id}/approve": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["approveCoinSellerSubApplication"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/coin-seller-sub-applications/{application_id}/reject": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["rejectCoinSellerSubApplication"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/coin-sellers/{user_id}/stock-credits": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@ -4096,6 +4160,12 @@ export interface components {
|
||||
pageSize: number;
|
||||
total: number;
|
||||
};
|
||||
ApiPageCoinSellerSubApplication: {
|
||||
items: components["schemas"]["CoinSellerSubApplication"][];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
};
|
||||
ApiPageCoinSellerLedger: {
|
||||
items: components["schemas"]["CoinSellerLedger"][];
|
||||
page: number;
|
||||
@ -4255,6 +4325,12 @@ export interface components {
|
||||
ApiResponseCoinSeller: components["schemas"]["Envelope"] & {
|
||||
data?: components["schemas"]["CoinSeller"];
|
||||
};
|
||||
ApiResponseCoinSellerSubApplicationPage: components["schemas"]["Envelope"] & {
|
||||
data?: components["schemas"]["ApiPageCoinSellerSubApplication"];
|
||||
};
|
||||
ApiResponseCoinSellerSubApplicationReview: components["schemas"]["Envelope"] & {
|
||||
data?: components["schemas"]["CoinSellerSubApplicationReviewResult"];
|
||||
};
|
||||
ApiResponseCoinSellerLedgerPage: components["schemas"]["Envelope"] & {
|
||||
data?: components["schemas"]["ApiPageCoinSellerLedger"];
|
||||
};
|
||||
@ -4492,11 +4568,79 @@ export interface components {
|
||||
regionName?: string;
|
||||
/** @enum {string} */
|
||||
status?: "active" | "disabled";
|
||||
subCoinSellerCount?: number;
|
||||
totalRechargeUsdtMicro?: number;
|
||||
updatedAtMs?: number;
|
||||
userId: string;
|
||||
username?: string;
|
||||
};
|
||||
CoinSellerSubApplicationUser: {
|
||||
avatar?: string;
|
||||
displayUserId?: string;
|
||||
regionId?: number;
|
||||
regionName?: string;
|
||||
userId: string;
|
||||
username?: string;
|
||||
};
|
||||
CoinSellerSubApplication: {
|
||||
applicationId: string;
|
||||
commandId?: string;
|
||||
createdAtMs?: number;
|
||||
parent: components["schemas"]["CoinSellerSubApplicationUser"];
|
||||
relationId?: string;
|
||||
reviewedAtMs?: number;
|
||||
reviewedByAdminId?: string;
|
||||
reviewReason?: string;
|
||||
/** @enum {string} */
|
||||
status: "pending" | "approved" | "rejected";
|
||||
target: components["schemas"]["CoinSellerSubApplicationUser"];
|
||||
updatedAtMs?: number;
|
||||
};
|
||||
CoinSellerSubApplicationReviewApplication: {
|
||||
applicationId: string;
|
||||
commandId?: string;
|
||||
createdAtMs?: number;
|
||||
parentUserId: string;
|
||||
relationId?: string;
|
||||
reviewedAtMs?: number;
|
||||
reviewedByAdminId?: string;
|
||||
reviewReason?: string;
|
||||
/** @enum {string} */
|
||||
status: "pending" | "approved" | "rejected";
|
||||
targetUserId: string;
|
||||
updatedAtMs?: number;
|
||||
};
|
||||
CoinSellerSubRelation: {
|
||||
childUserId: string;
|
||||
createdAtMs?: number;
|
||||
createdByUserId?: string;
|
||||
parentUserId: string;
|
||||
relationId: string;
|
||||
status: string;
|
||||
updatedAtMs?: number;
|
||||
};
|
||||
CoinSellerSubApplicationReviewChild: {
|
||||
avatar?: string;
|
||||
canManageSubCoinSellers?: boolean;
|
||||
contactInfo?: string;
|
||||
displayUserId?: string;
|
||||
merchantAssetType?: string;
|
||||
regionId?: number;
|
||||
regionName?: string;
|
||||
status?: string;
|
||||
updatedAtMs?: number;
|
||||
userId: string;
|
||||
username?: string;
|
||||
};
|
||||
CoinSellerSubApplicationReviewPayload: {
|
||||
commandId: string;
|
||||
reason?: string;
|
||||
};
|
||||
CoinSellerSubApplicationReviewResult: {
|
||||
application?: components["schemas"]["CoinSellerSubApplicationReviewApplication"];
|
||||
child?: components["schemas"]["CoinSellerSubApplicationReviewChild"];
|
||||
relation?: components["schemas"]["CoinSellerSubRelation"];
|
||||
};
|
||||
CoinLedgerUser: {
|
||||
avatar?: string;
|
||||
displayUserId?: string;
|
||||
@ -4509,7 +4653,15 @@ export interface components {
|
||||
/** Format: int64 */
|
||||
availableDelta: number;
|
||||
/** @enum {string} */
|
||||
bizType: "coin_seller_stock_purchase" | "coin_seller_stock_deduction" | "coin_seller_recharge" | "coin_seller_coin_compensation" | "coin_seller_transfer" | "coin_seller_sub_transfer" | "salary_transfer_to_coin_seller" | "manual_credit";
|
||||
bizType:
|
||||
| "coin_seller_stock_purchase"
|
||||
| "coin_seller_stock_deduction"
|
||||
| "coin_seller_recharge"
|
||||
| "coin_seller_coin_compensation"
|
||||
| "coin_seller_transfer"
|
||||
| "coin_seller_sub_transfer"
|
||||
| "salary_transfer_to_coin_seller"
|
||||
| "manual_credit";
|
||||
commandId?: string;
|
||||
counterpartyUserId?: string;
|
||||
/** Format: int64 */
|
||||
@ -4841,7 +4993,13 @@ export interface components {
|
||||
appCode: string;
|
||||
appName?: string;
|
||||
/** @enum {string} */
|
||||
operation: "user_coin_credit" | "user_coin_debit" | "user_wallet_debit" | "user_wallet_credit" | "coin_seller_coin_credit" | "coin_seller_coin_debit";
|
||||
operation:
|
||||
| "user_coin_credit"
|
||||
| "user_coin_debit"
|
||||
| "user_wallet_debit"
|
||||
| "user_wallet_credit"
|
||||
| "coin_seller_coin_credit"
|
||||
| "coin_seller_coin_debit";
|
||||
walletIdentity?: string;
|
||||
targetUserId: string;
|
||||
coinAmount: number;
|
||||
@ -4923,7 +5081,13 @@ export interface components {
|
||||
FinanceApplicationInput: {
|
||||
appCode: string;
|
||||
/** @enum {string} */
|
||||
operation: "user_coin_credit" | "user_coin_debit" | "user_wallet_debit" | "user_wallet_credit" | "coin_seller_coin_credit" | "coin_seller_coin_debit";
|
||||
operation:
|
||||
| "user_coin_credit"
|
||||
| "user_coin_debit"
|
||||
| "user_wallet_debit"
|
||||
| "user_wallet_credit"
|
||||
| "coin_seller_coin_credit"
|
||||
| "coin_seller_coin_debit";
|
||||
/** @enum {string} */
|
||||
walletIdentity?: "host" | "agency" | "bd";
|
||||
targetUserId: string;
|
||||
@ -4940,6 +5104,8 @@ export interface components {
|
||||
/** @enum {string} */
|
||||
providerCode: "mifapay" | "v5pay" | "usdt";
|
||||
externalOrderNo: string;
|
||||
providerCurrencyCode?: string;
|
||||
providerAmountMinor?: number;
|
||||
/** @enum {string} */
|
||||
chain?: "TRON" | "BSC";
|
||||
remark?: string;
|
||||
@ -4968,6 +5134,8 @@ export interface components {
|
||||
chain?: "TRON" | "BSC";
|
||||
usdAmount?: number;
|
||||
usdMinorAmount?: number;
|
||||
providerCurrencyCode?: string;
|
||||
providerAmountMinor?: number;
|
||||
};
|
||||
UserFinanceScope: {
|
||||
appCode: string;
|
||||
@ -5021,6 +5189,24 @@ export interface components {
|
||||
};
|
||||
};
|
||||
/** @description OK */
|
||||
CoinSellerSubApplicationPageResponse: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ApiResponseCoinSellerSubApplicationPage"];
|
||||
};
|
||||
};
|
||||
/** @description OK */
|
||||
CoinSellerSubApplicationReviewResponse: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ApiResponseCoinSellerSubApplicationReview"];
|
||||
};
|
||||
};
|
||||
/** @description OK */
|
||||
CoinSellerLedgerPageResponse: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
@ -5398,6 +5584,11 @@ export interface components {
|
||||
"application/json": components["schemas"]["CoinSellerStatusPayload"];
|
||||
};
|
||||
};
|
||||
CoinSellerSubApplicationReviewRequest: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["CoinSellerSubApplicationReviewPayload"];
|
||||
};
|
||||
};
|
||||
CountryCreateRequest: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["CountryCreateInput"];
|
||||
@ -6893,6 +7084,70 @@ export interface operations {
|
||||
200: components["responses"]["CoinSellerResponse"];
|
||||
};
|
||||
};
|
||||
listCoinSellerSubSellers: {
|
||||
parameters: {
|
||||
query?: {
|
||||
page?: components["parameters"]["Page"];
|
||||
page_size?: components["parameters"]["PageSize"];
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
user_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["CoinSellerPageResponse"];
|
||||
};
|
||||
};
|
||||
listCoinSellerSubApplications: {
|
||||
parameters: {
|
||||
query?: {
|
||||
page?: components["parameters"]["Page"];
|
||||
page_size?: components["parameters"]["PageSize"];
|
||||
keyword?: components["parameters"]["Keyword"];
|
||||
status?: "pending" | "approved" | "rejected";
|
||||
parent_user_id?: string;
|
||||
target_user_id?: string;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["CoinSellerSubApplicationPageResponse"];
|
||||
};
|
||||
};
|
||||
approveCoinSellerSubApplication: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
application_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: components["requestBodies"]["CoinSellerSubApplicationReviewRequest"];
|
||||
responses: {
|
||||
200: components["responses"]["CoinSellerSubApplicationReviewResponse"];
|
||||
};
|
||||
};
|
||||
rejectCoinSellerSubApplication: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
application_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: components["requestBodies"]["CoinSellerSubApplicationReviewRequest"];
|
||||
responses: {
|
||||
200: components["responses"]["CoinSellerSubApplicationReviewResponse"];
|
||||
};
|
||||
};
|
||||
creditCoinSellerStock: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@ -7770,7 +8025,11 @@ export interface operations {
|
||||
seller_filter_user_id?: string;
|
||||
seller_display_user_id?: string;
|
||||
seller_username?: string;
|
||||
ledger_type?: "admin_stock_credit" | "seller_transfer" | "sub_seller_transfer" | "salary_transfer_received";
|
||||
ledger_type?:
|
||||
| "admin_stock_credit"
|
||||
| "seller_transfer"
|
||||
| "sub_seller_transfer"
|
||||
| "salary_transfer_received";
|
||||
start_at_ms?: number;
|
||||
end_at_ms?: number;
|
||||
};
|
||||
@ -7793,7 +8052,11 @@ export interface operations {
|
||||
seller_filter_user_id?: string;
|
||||
seller_display_user_id?: string;
|
||||
seller_username?: string;
|
||||
ledger_type?: "admin_stock_credit" | "seller_transfer" | "sub_seller_transfer" | "salary_transfer_received";
|
||||
ledger_type?:
|
||||
| "admin_stock_credit"
|
||||
| "seller_transfer"
|
||||
| "sub_seller_transfer"
|
||||
| "salary_transfer_received";
|
||||
start_at_ms?: number;
|
||||
end_at_ms?: number;
|
||||
};
|
||||
@ -7847,7 +8110,13 @@ export interface operations {
|
||||
sent_at_ms?: number;
|
||||
summary: string;
|
||||
/** @enum {string} */
|
||||
target_scope: "all_registered_users" | "all_active_users" | "single_user" | "user_ids" | "region" | "country";
|
||||
target_scope:
|
||||
| "all_registered_users"
|
||||
| "all_active_users"
|
||||
| "single_user"
|
||||
| "user_ids"
|
||||
| "region"
|
||||
| "country";
|
||||
/** Format: int64 */
|
||||
target_user_id?: number;
|
||||
title: string;
|
||||
|
||||
@ -628,7 +628,9 @@ export interface FinanceCoinSellerRechargeOrderPayload {
|
||||
chain?: string;
|
||||
coinAmount: number;
|
||||
externalOrderNo: string;
|
||||
providerAmountMinor?: number;
|
||||
providerCode: "mifapay" | "v5pay" | "usdt";
|
||||
providerCurrencyCode?: string;
|
||||
remark?: string;
|
||||
targetUserId: EntityId;
|
||||
usdAmount: number;
|
||||
@ -638,7 +640,9 @@ export interface FinanceCoinSellerRechargeReceiptVerificationPayload {
|
||||
appCode: string;
|
||||
chain?: string;
|
||||
externalOrderNo: string;
|
||||
providerAmountMinor?: number;
|
||||
providerCode: "mifapay" | "v5pay" | "usdt";
|
||||
providerCurrencyCode?: string;
|
||||
usdAmount?: number;
|
||||
usdMinorAmount?: number;
|
||||
}
|
||||
@ -842,12 +846,85 @@ export interface CoinSellerDto {
|
||||
regionId?: number;
|
||||
regionName?: string;
|
||||
status?: string;
|
||||
subCoinSellerCount?: number;
|
||||
totalRechargeUsdtMicro?: number;
|
||||
updatedAtMs?: number;
|
||||
userId: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export interface CoinSellerSubApplicationUserDto {
|
||||
avatar?: string;
|
||||
displayUserId?: string;
|
||||
regionId?: number;
|
||||
regionName?: string;
|
||||
userId: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export interface CoinSellerSubApplicationDto {
|
||||
applicationId: string;
|
||||
commandId?: string;
|
||||
createdAtMs?: number;
|
||||
parent: CoinSellerSubApplicationUserDto;
|
||||
relationId?: string;
|
||||
reviewedAtMs?: number;
|
||||
reviewedByAdminId?: string;
|
||||
reviewReason?: string;
|
||||
status: "pending" | "approved" | "rejected";
|
||||
target: CoinSellerSubApplicationUserDto;
|
||||
updatedAtMs?: number;
|
||||
}
|
||||
|
||||
export interface CoinSellerSubApplicationReviewPayload {
|
||||
commandId: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface CoinSellerSubApplicationReviewApplicationDto {
|
||||
applicationId: string;
|
||||
commandId?: string;
|
||||
createdAtMs?: number;
|
||||
parentUserId: string;
|
||||
relationId?: string;
|
||||
reviewedAtMs?: number;
|
||||
reviewedByAdminId?: string;
|
||||
reviewReason?: string;
|
||||
status: "pending" | "approved" | "rejected";
|
||||
targetUserId: string;
|
||||
updatedAtMs?: number;
|
||||
}
|
||||
|
||||
export interface CoinSellerSubRelationDto {
|
||||
childUserId: string;
|
||||
createdAtMs?: number;
|
||||
createdByUserId?: string;
|
||||
parentUserId: string;
|
||||
relationId: string;
|
||||
status: string;
|
||||
updatedAtMs?: number;
|
||||
}
|
||||
|
||||
export interface CoinSellerSubApplicationReviewChildDto {
|
||||
avatar?: string;
|
||||
canManageSubCoinSellers?: boolean;
|
||||
contactInfo?: string;
|
||||
displayUserId?: string;
|
||||
merchantAssetType?: string;
|
||||
regionId?: number;
|
||||
regionName?: string;
|
||||
status?: string;
|
||||
updatedAtMs?: number;
|
||||
userId: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export interface CoinSellerSubApplicationReviewResultDto {
|
||||
application?: CoinSellerSubApplicationReviewApplicationDto;
|
||||
child?: CoinSellerSubApplicationReviewChildDto;
|
||||
relation?: CoinSellerSubRelationDto;
|
||||
}
|
||||
|
||||
export interface CoinSellerStockCreditPayload {
|
||||
coinAmount: number;
|
||||
commandId: string;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user