子币商以及 充值流水相关
This commit is contained in:
parent
c7ca4f23ec
commit
cf98d5bd9a
@ -1622,6 +1622,129 @@
|
|||||||
"x-permissions": ["coin-seller:update"]
|
"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": {
|
"/admin/coin-sellers/{user_id}/stock-credits": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "creditCoinSellerStock",
|
"operationId": "creditCoinSellerStock",
|
||||||
@ -2887,7 +3010,12 @@
|
|||||||
"in": "query",
|
"in": "query",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string",
|
"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",
|
"in": "query",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string",
|
"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": "keyword", "schema": { "type": "string" } },
|
||||||
{ "in": "query", "name": "status", "schema": { "type": "string", "enum": ["active", "disabled"] } },
|
{ "in": "query", "name": "status", "schema": { "type": "string", "enum": ["active", "disabled"] } },
|
||||||
{ "in": "query", "name": "page", "schema": { "type": "integer", "minimum": 1 } },
|
{ "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": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
@ -3161,7 +3298,11 @@
|
|||||||
{ "in": "query", "name": "keyword", "schema": { "type": "string" } },
|
{ "in": "query", "name": "keyword", "schema": { "type": "string" } },
|
||||||
{ "in": "query", "name": "status", "schema": { "type": "string", "enum": ["active", "disabled"] } },
|
{ "in": "query", "name": "status", "schema": { "type": "string", "enum": ["active", "disabled"] } },
|
||||||
{ "in": "query", "name": "page", "schema": { "type": "integer", "minimum": 1 } },
|
{ "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": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
@ -3718,7 +3859,12 @@
|
|||||||
{ "in": "query", "name": "end_ms", "schema": { "type": "integer", "format": "int64" } },
|
{ "in": "query", "name": "end_ms", "schema": { "type": "integer", "format": "int64" } },
|
||||||
{ "in": "query", "name": "app_codes", "schema": { "type": "string" } },
|
{ "in": "query", "name": "app_codes", "schema": { "type": "string" } },
|
||||||
{ "in": "query", "name": "region_id", "schema": { "type": "integer", "format": "int64" } },
|
{ "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",
|
"in": "query",
|
||||||
"name": "section",
|
"name": "section",
|
||||||
@ -6728,6 +6874,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"CoinSellerSubApplicationReviewRequest": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CoinSellerSubApplicationReviewPayload"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"CountryCreateRequest": {
|
"CountryCreateRequest": {
|
||||||
"required": true,
|
"required": true,
|
||||||
"content": {
|
"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": {
|
"CoinSellerLedgerPageResponse": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"content": {
|
"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": {
|
"ApiPageCoinSellerLedger": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["items", "page", "pageSize", "total"],
|
"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": {
|
"ApiResponseCoinSellerLedgerPage": {
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
@ -8780,6 +9007,9 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["active", "disabled"]
|
"enum": ["active", "disabled"]
|
||||||
},
|
},
|
||||||
|
"subCoinSellerCount": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"totalRechargeUsdtMicro": {
|
"totalRechargeUsdtMicro": {
|
||||||
"type": "integer"
|
"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": {
|
"CoinLedgerUser": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["userId"],
|
"required": ["userId"],
|
||||||
@ -8814,7 +9242,19 @@
|
|||||||
},
|
},
|
||||||
"CoinSellerLedger": {
|
"CoinSellerLedger": {
|
||||||
"type": "object",
|
"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": {
|
"properties": {
|
||||||
"amount": {
|
"amount": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@ -8857,7 +9297,12 @@
|
|||||||
},
|
},
|
||||||
"ledgerType": {
|
"ledgerType": {
|
||||||
"type": "string",
|
"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": {
|
"metadata": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@ -10132,6 +10577,13 @@
|
|||||||
"externalOrderNo": {
|
"externalOrderNo": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"providerCurrencyCode": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"providerAmountMinor": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
"chain": {
|
"chain": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["TRON", "BSC"]
|
"enum": ["TRON", "BSC"]
|
||||||
@ -10210,6 +10662,13 @@
|
|||||||
"usdMinorAmount": {
|
"usdMinorAmount": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"minimum": 0
|
"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",
|
appCode: "lalu",
|
||||||
coinAmount: 100000,
|
coinAmount: 100000,
|
||||||
externalOrderNo: "MIFA-001",
|
externalOrderNo: "MIFA-001",
|
||||||
|
providerAmountMinor: 125050,
|
||||||
providerCode: "mifapay",
|
providerCode: "mifapay",
|
||||||
|
providerCurrencyCode: "PHP",
|
||||||
targetUserId: "10001",
|
targetUserId: "10001",
|
||||||
usdAmount: 10
|
usdAmount: 10
|
||||||
});
|
});
|
||||||
await verifyFinanceCoinSellerRechargeReceipt({
|
await verifyFinanceCoinSellerRechargeReceipt({
|
||||||
appCode: "lalu",
|
appCode: "lalu",
|
||||||
externalOrderNo: "MIFA-001",
|
externalOrderNo: "MIFA-001",
|
||||||
|
providerAmountMinor: 125050,
|
||||||
providerCode: "mifapay",
|
providerCode: "mifapay",
|
||||||
|
providerCurrencyCode: "PHP",
|
||||||
usdAmount: 10
|
usdAmount: 10
|
||||||
});
|
});
|
||||||
await verifyFinanceCoinSellerRechargeOrder("order-1");
|
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(calls[0][1]?.method).toBe("GET");
|
||||||
expect(String(calls[1][0])).toContain("/api/v1/admin/finance/orders/coin-seller-recharges");
|
expect(String(calls[1][0])).toContain("/api/v1/admin/finance/orders/coin-seller-recharges");
|
||||||
expect(calls[1][1]?.method).toBe("POST");
|
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(String(calls[2][0])).toContain("/api/v1/admin/finance/orders/coin-seller-recharges/receipt-verifications");
|
||||||
expect(calls[2][1]?.method).toBe("POST");
|
expect(calls[2][1]?.method).toBe("POST");
|
||||||
expect(JSON.parse(String(calls[2][1]?.body))).toEqual({
|
expect(JSON.parse(String(calls[2][1]?.body))).toEqual({
|
||||||
appCode: "lalu",
|
appCode: "lalu",
|
||||||
externalOrderNo: "MIFA-001",
|
externalOrderNo: "MIFA-001",
|
||||||
|
providerAmountMinor: 125050,
|
||||||
providerCode: "mifapay",
|
providerCode: "mifapay",
|
||||||
|
providerCurrencyCode: "PHP",
|
||||||
usdAmount: 10
|
usdAmount: 10
|
||||||
});
|
});
|
||||||
expect(String(calls[3][0])).toContain("/api/v1/admin/finance/orders/coin-seller-recharges/order-1/verify");
|
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 PaidOutlined from "@mui/icons-material/PaidOutlined";
|
||||||
import RefreshOutlined from "@mui/icons-material/RefreshOutlined";
|
import RefreshOutlined from "@mui/icons-material/RefreshOutlined";
|
||||||
import Alert from "@mui/material/Alert";
|
import Alert from "@mui/material/Alert";
|
||||||
|
import Autocomplete from "@mui/material/Autocomplete";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Dialog from "@mui/material/Dialog";
|
import Dialog from "@mui/material/Dialog";
|
||||||
import DialogActions from "@mui/material/DialogActions";
|
import DialogActions from "@mui/material/DialogActions";
|
||||||
@ -14,6 +15,7 @@ import TextField from "@mui/material/TextField";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
COIN_SELLER_RECHARGE_CHAINS,
|
COIN_SELLER_RECHARGE_CHAINS,
|
||||||
|
COIN_SELLER_RECHARGE_PROVIDER_CURRENCIES,
|
||||||
COIN_SELLER_RECHARGE_PROVIDERS,
|
COIN_SELLER_RECHARGE_PROVIDERS,
|
||||||
COIN_SELLER_RECHARGE_STATUS,
|
COIN_SELLER_RECHARGE_STATUS,
|
||||||
DEFAULT_COIN_SELLER_RECHARGE_ORDER_FORM,
|
DEFAULT_COIN_SELLER_RECHARGE_ORDER_FORM,
|
||||||
@ -78,7 +80,9 @@ export function FinanceCoinSellerRechargeOrderList({
|
|||||||
setForm((current) => ({
|
setForm((current) => ({
|
||||||
...current,
|
...current,
|
||||||
chain: "",
|
chain: "",
|
||||||
|
providerAmount: "",
|
||||||
providerCode: value,
|
providerCode: value,
|
||||||
|
providerCurrencyCode: "",
|
||||||
}));
|
}));
|
||||||
setFormError("");
|
setFormError("");
|
||||||
setReceiptVerification(null);
|
setReceiptVerification(null);
|
||||||
@ -201,7 +205,12 @@ export function FinanceCoinSellerRechargeOrderList({
|
|||||||
<Button startIcon={<RefreshOutlined fontSize="small" />} variant="outlined" onClick={onReload}>
|
<Button startIcon={<RefreshOutlined fontSize="small" />} variant="outlined" onClick={onReload}>
|
||||||
刷新
|
刷新
|
||||||
</Button>
|
</Button>
|
||||||
<Button disabled={!canCreate} startIcon={<AddOutlined fontSize="small" />} variant="contained" onClick={openCreate}>
|
<Button
|
||||||
|
disabled={!canCreate}
|
||||||
|
startIcon={<AddOutlined fontSize="small" />}
|
||||||
|
variant="contained"
|
||||||
|
onClick={openCreate}
|
||||||
|
>
|
||||||
创建币商充值
|
创建币商充值
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -234,7 +243,10 @@ export function FinanceCoinSellerRechargeOrderList({
|
|||||||
<td className="finance-order-cell">{item.id || "-"}</td>
|
<td className="finance-order-cell">{item.id || "-"}</td>
|
||||||
<td>{appName(item.appCode, apps)}</td>
|
<td>{appName(item.appCode, apps)}</td>
|
||||||
<td>
|
<td>
|
||||||
<Stacked primary={item.targetDisplayUserId || item.targetUserId} secondary={item.targetUserId} />
|
<Stacked
|
||||||
|
primary={item.targetDisplayUserId || item.targetUserId}
|
||||||
|
secondary={item.targetUserId}
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="r num">
|
<td className="r num">
|
||||||
<Stacked
|
<Stacked
|
||||||
@ -243,10 +255,16 @@ export function FinanceCoinSellerRechargeOrderList({
|
|||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Stacked primary={coinSellerRechargeProviderLabel(item.providerCode)} secondary={providerMeta(item)} />
|
<Stacked
|
||||||
|
primary={coinSellerRechargeProviderLabel(item.providerCode)}
|
||||||
|
secondary={providerMeta(item)}
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Stacked primary={item.externalOrderNo || "-"} secondary={providerAmountText(item)} />
|
<Stacked
|
||||||
|
primary={item.externalOrderNo || "-"}
|
||||||
|
secondary={providerAmountText(item)}
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="finance-remark-cell">{item.remark || "-"}</td>
|
<td className="finance-remark-cell">{item.remark || "-"}</td>
|
||||||
<td>
|
<td>
|
||||||
@ -266,7 +284,8 @@ export function FinanceCoinSellerRechargeOrderList({
|
|||||||
primary={item.operatorName || "-"}
|
primary={item.operatorName || "-"}
|
||||||
secondary={[
|
secondary={[
|
||||||
item.verifiedByName && `校验:${item.verifiedByName}`,
|
item.verifiedByName && `校验:${item.verifiedByName}`,
|
||||||
item.grantedByName && `${isZeroCoinMakeupOrder(item) ? "补单" : "发放"}:${item.grantedByName}`,
|
item.grantedByName &&
|
||||||
|
`${isZeroCoinMakeupOrder(item) ? "补单" : "发放"}:${item.grantedByName}`,
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(" / ")}
|
.join(" / ")}
|
||||||
@ -277,7 +296,8 @@ export function FinanceCoinSellerRechargeOrderList({
|
|||||||
primary={formatTime(item.createdAtMs)}
|
primary={formatTime(item.createdAtMs)}
|
||||||
secondary={[
|
secondary={[
|
||||||
item.verifiedAtMs && `校验 ${formatTime(item.verifiedAtMs)}`,
|
item.verifiedAtMs && `校验 ${formatTime(item.verifiedAtMs)}`,
|
||||||
item.grantedAtMs && `${isZeroCoinMakeupOrder(item) ? "补单" : "发放"} ${formatTime(item.grantedAtMs)}`,
|
item.grantedAtMs &&
|
||||||
|
`${isZeroCoinMakeupOrder(item) ? "补单" : "发放"} ${formatTime(item.grantedAtMs)}`,
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(" / ")}
|
.join(" / ")}
|
||||||
@ -330,13 +350,21 @@ export function FinanceCoinSellerRechargeOrderList({
|
|||||||
) : null}
|
) : null}
|
||||||
{total > pageSize ? (
|
{total > pageSize ? (
|
||||||
<div className="finance-pagination">
|
<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>
|
</Button>
|
||||||
<span>
|
<span>
|
||||||
第 {page} 页 / 共 {total} 条
|
第 {page} 页 / 共 {total} 条
|
||||||
</span>
|
</span>
|
||||||
<Button disabled={!hasNextPage || loading} variant="outlined" onClick={() => onFiltersChange({ page: page + 1 })}>
|
<Button
|
||||||
|
disabled={!hasNextPage || loading}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => onFiltersChange({ page: page + 1 })}
|
||||||
|
>
|
||||||
下一页
|
下一页
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -379,14 +407,37 @@ function CreateDialog({
|
|||||||
const canCreate = receiptVerification?.verified === true && !disabled;
|
const canCreate = receiptVerification?.verified === true && !disabled;
|
||||||
|
|
||||||
return (
|
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>
|
<DialogTitle>创建币商充值</DialogTitle>
|
||||||
<DialogContent className="finance-create-dialog">
|
<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}
|
{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 ? (
|
{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) => (
|
{apps.map((app) => (
|
||||||
<MenuItem key={app.appCode} value={app.appCode}>
|
<MenuItem key={app.appCode} value={app.appCode}>
|
||||||
{app.appName || app.appCode}
|
{app.appName || app.appCode}
|
||||||
@ -394,9 +445,21 @@ function CreateDialog({
|
|||||||
))}
|
))}
|
||||||
</TextField>
|
</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
|
<TextField
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
InputProps={{ startAdornment: <InputAdornment position="start">USD</InputAdornment> }}
|
InputProps={{ startAdornment: <InputAdornment position="start">USD</InputAdornment> }}
|
||||||
@ -417,7 +480,14 @@ function CreateDialog({
|
|||||||
value={form.coinAmount}
|
value={form.coinAmount}
|
||||||
onChange={(event) => onChange("coinAmount", event.target.value)}
|
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]) => (
|
{COIN_SELLER_RECHARGE_PROVIDERS.map(([value, label]) => (
|
||||||
<MenuItem key={value} value={value}>
|
<MenuItem key={value} value={value}>
|
||||||
{label}
|
{label}
|
||||||
@ -425,7 +495,14 @@ function CreateDialog({
|
|||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
{form.providerCode === "usdt" ? (
|
{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]) => (
|
{COIN_SELLER_RECHARGE_CHAINS.map(([value, label]) => (
|
||||||
<MenuItem key={value} value={value}>
|
<MenuItem key={value} value={value}>
|
||||||
{label}
|
{label}
|
||||||
@ -433,9 +510,45 @@ function CreateDialog({
|
|||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
) : null}
|
) : 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">
|
<div className="finance-receipt-field">
|
||||||
<TextField disabled={disabled} label="三方订单号" required value={form.externalOrderNo} onChange={(event) => onChange("externalOrderNo", event.target.value)} />
|
<TextField
|
||||||
<Button disabled={disabled || !onVerifyReceipt} startIcon={<FactCheckOutlined fontSize="small" />} variant="outlined" onClick={onVerifyReceipt}>
|
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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -466,6 +579,14 @@ function isZeroCoinMakeupOrder(order) {
|
|||||||
return Number(order?.coinAmount || 0) === 0;
|
return Number(order?.coinAmount || 0) === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function providerCurrencyOptions(providerCode) {
|
||||||
|
return COIN_SELLER_RECHARGE_PROVIDER_CURRENCIES[providerCode] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function currencyOptionLabel(option) {
|
||||||
|
return option || "";
|
||||||
|
}
|
||||||
|
|
||||||
function ReceiptVerificationBanner({ verification }) {
|
function ReceiptVerificationBanner({ verification }) {
|
||||||
const detail = [
|
const detail = [
|
||||||
verification.status && `状态 ${verification.status}`,
|
verification.status && `状态 ${verification.status}`,
|
||||||
@ -486,7 +607,11 @@ function ReceiptVerificationBanner({ verification }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function StatusBadge({ status }) {
|
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 }) {
|
function Stacked({ primary, secondary }) {
|
||||||
@ -523,7 +648,9 @@ function providerMeta(item) {
|
|||||||
return [item.providerStatus, item.providerOrderId].filter(Boolean).join(" / ") || "-";
|
return [item.providerStatus, item.providerOrderId].filter(Boolean).join(" / ") || "-";
|
||||||
}
|
}
|
||||||
if (item.providerCode === "v5pay") {
|
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 "-";
|
return "-";
|
||||||
}
|
}
|
||||||
@ -531,8 +658,12 @@ function providerMeta(item) {
|
|||||||
function walletMeta(item) {
|
function walletMeta(item) {
|
||||||
return [
|
return [
|
||||||
item.walletAssetType,
|
item.walletAssetType,
|
||||||
item.walletAmountDelta === null || item.walletAmountDelta === undefined ? "" : `变动 ${formatAmount(item.walletAmountDelta)}`,
|
item.walletAmountDelta === null || item.walletAmountDelta === undefined
|
||||||
item.walletBalanceAfter === null || item.walletBalanceAfter === undefined ? "" : `余额 ${formatAmount(item.walletBalanceAfter)}`,
|
? ""
|
||||||
|
: `变动 ${formatAmount(item.walletAmountDelta)}`,
|
||||||
|
item.walletBalanceAfter === null || item.walletBalanceAfter === undefined
|
||||||
|
? ""
|
||||||
|
: `余额 ${formatAmount(item.walletBalanceAfter)}`,
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(" / ");
|
.join(" / ");
|
||||||
|
|||||||
@ -9,26 +9,31 @@ afterEach(() => {
|
|||||||
test("coin seller recharge order create dialog verifies receipt before create", async () => {
|
test("coin seller recharge order create dialog verifies receipt before create", async () => {
|
||||||
const onCreate = vi.fn(async () => ({ id: "order-2" }));
|
const onCreate = vi.fn(async () => ({ id: "order-2" }));
|
||||||
const onVerifyReceipt = vi.fn(async () => ({
|
const onVerifyReceipt = vi.fn(async () => ({
|
||||||
currencyCode: "USD",
|
currencyCode: "PHP",
|
||||||
providerAmountMinor: 1000,
|
providerAmountMinor: 4800,
|
||||||
providerOrderId: "V5-001",
|
providerOrderId: "V5-001",
|
||||||
status: "paid",
|
status: "paid",
|
||||||
verified: true,
|
verified: true,
|
||||||
}));
|
}));
|
||||||
render(<FinanceCoinSellerRechargeOrderList {...baseProps()} onCreate={onCreate} onVerifyReceipt={onVerifyReceipt} />);
|
render(
|
||||||
|
<FinanceCoinSellerRechargeOrderList {...baseProps()} onCreate={onCreate} onVerifyReceipt={onVerifyReceipt} />,
|
||||||
|
);
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "创建币商充值" }));
|
fireEvent.click(screen.getByRole("button", { name: "创建币商充值" }));
|
||||||
fireEvent.mouseDown(screen.getByRole("combobox", { name: "支付渠道" }));
|
fireEvent.mouseDown(screen.getByRole("combobox", { name: "支付渠道" }));
|
||||||
fireEvent.click(screen.getByRole("option", { name: "V5Pay" }));
|
fireEvent.click(screen.getByRole("option", { name: "V5Pay" }));
|
||||||
|
|
||||||
expect(screen.queryByRole("textbox", { name: "三方国家码" })).not.toBeInTheDocument();
|
expect(screen.getByRole("combobox", { name: "币种" })).toBeInTheDocument();
|
||||||
expect(screen.queryByRole("textbox", { name: "三方币种" })).not.toBeInTheDocument();
|
expect(screen.getByRole("spinbutton", { name: "币种金额" })).toBeInTheDocument();
|
||||||
expect(screen.queryByRole("textbox", { name: "支付类型" })).not.toBeInTheDocument();
|
expect(screen.queryByRole("textbox", { name: "支付类型" })).not.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("textbox", { name: "目标用户 ID" }), { target: { value: "10001" } });
|
||||||
fireEvent.change(screen.getByRole("spinbutton", { name: "USD 金额" }), { target: { value: "10" } });
|
fireEvent.change(screen.getByRole("spinbutton", { name: "USD 金额" }), { target: { value: "10" } });
|
||||||
fireEvent.change(screen.getByRole("spinbutton", { name: "充值金币" }), { target: { value: "0" } });
|
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: "V5-001" } });
|
||||||
fireEvent.change(screen.getByRole("textbox", { name: "备注" }), { target: { value: "补单,不发金币" } });
|
fireEvent.change(screen.getByRole("textbox", { name: "备注" }), { target: { value: "补单,不发金币" } });
|
||||||
expect(screen.getByRole("button", { name: "创建" })).toBeDisabled();
|
expect(screen.getByRole("button", { name: "创建" })).toBeDisabled();
|
||||||
@ -39,7 +44,9 @@ test("coin seller recharge order create dialog verifies receipt before create",
|
|||||||
expect(onVerifyReceipt).toHaveBeenCalledWith({
|
expect(onVerifyReceipt).toHaveBeenCalledWith({
|
||||||
appCode: "lalu",
|
appCode: "lalu",
|
||||||
externalOrderNo: "V5-001",
|
externalOrderNo: "V5-001",
|
||||||
|
providerAmountMinor: 4800,
|
||||||
providerCode: "v5pay",
|
providerCode: "v5pay",
|
||||||
|
providerCurrencyCode: "PHP",
|
||||||
usdAmount: 10,
|
usdAmount: 10,
|
||||||
});
|
});
|
||||||
fireEvent.click(screen.getByRole("button", { name: "创建" }));
|
fireEvent.click(screen.getByRole("button", { name: "创建" }));
|
||||||
@ -48,7 +55,9 @@ test("coin seller recharge order create dialog verifies receipt before create",
|
|||||||
appCode: "lalu",
|
appCode: "lalu",
|
||||||
coinAmount: 0,
|
coinAmount: 0,
|
||||||
externalOrderNo: "V5-001",
|
externalOrderNo: "V5-001",
|
||||||
|
providerAmountMinor: 4800,
|
||||||
providerCode: "v5pay",
|
providerCode: "v5pay",
|
||||||
|
providerCurrencyCode: "PHP",
|
||||||
remark: "补单,不发金币",
|
remark: "补单,不发金币",
|
||||||
targetUserId: "10001",
|
targetUserId: "10001",
|
||||||
usdAmount: 10,
|
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", () => {
|
test("coin seller recharge order actions follow verify and grant status", () => {
|
||||||
const onVerify = vi.fn();
|
const onVerify = vi.fn();
|
||||||
const onGrant = vi.fn();
|
const onGrant = vi.fn();
|
||||||
vi.stubGlobal("confirm", vi.fn(() => true));
|
vi.stubGlobal(
|
||||||
|
"confirm",
|
||||||
|
vi.fn(() => true),
|
||||||
|
);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<FinanceCoinSellerRechargeOrderList
|
<FinanceCoinSellerRechargeOrderList
|
||||||
@ -100,7 +112,9 @@ test("zero coin seller recharge order uses makeup completion action", () => {
|
|||||||
<FinanceCoinSellerRechargeOrderList
|
<FinanceCoinSellerRechargeOrderList
|
||||||
{...baseProps({
|
{...baseProps({
|
||||||
orders: {
|
orders: {
|
||||||
items: [orderFixture({ coinAmount: 0, grantStatus: "pending", id: "makeup", verifyStatus: "verified" })],
|
items: [
|
||||||
|
orderFixture({ coinAmount: 0, grantStatus: "pending", id: "makeup", verifyStatus: "verified" }),
|
||||||
|
],
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 50,
|
pageSize: 50,
|
||||||
total: 1,
|
total: 1,
|
||||||
|
|||||||
@ -70,6 +70,11 @@ export const COIN_SELLER_RECHARGE_CHAINS = [
|
|||||||
["BSC", "BSC"],
|
["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 = [
|
export const COIN_SELLER_RECHARGE_STATUS = [
|
||||||
["pending", "待处理"],
|
["pending", "待处理"],
|
||||||
["verified", "已校验"],
|
["verified", "已校验"],
|
||||||
@ -106,7 +111,9 @@ export const DEFAULT_COIN_SELLER_RECHARGE_ORDER_FORM = {
|
|||||||
chain: "",
|
chain: "",
|
||||||
coinAmount: "",
|
coinAmount: "",
|
||||||
externalOrderNo: "",
|
externalOrderNo: "",
|
||||||
|
providerAmount: "",
|
||||||
providerCode: "mifapay",
|
providerCode: "mifapay",
|
||||||
|
providerCurrencyCode: "",
|
||||||
remark: "",
|
remark: "",
|
||||||
targetUserId: "",
|
targetUserId: "",
|
||||||
usdAmount: "",
|
usdAmount: "",
|
||||||
|
|||||||
@ -129,12 +129,16 @@ export function normalizeWithdrawalApplicationPage(data = {}) {
|
|||||||
|
|
||||||
export function buildCoinSellerRechargeOrderPayload(form) {
|
export function buildCoinSellerRechargeOrderPayload(form) {
|
||||||
const providerCode = stringValue(form.providerCode);
|
const providerCode = stringValue(form.providerCode);
|
||||||
|
const providerCurrencyCode = stringValue(form.providerCurrencyCode).toUpperCase();
|
||||||
|
const providerAmountMinor = providerAmountMinorFromForm(form);
|
||||||
const payload = {
|
const payload = {
|
||||||
appCode: stringValue(form.appCode),
|
appCode: stringValue(form.appCode),
|
||||||
chain: providerCode === "usdt" ? stringValue(form.chain).toUpperCase() : "",
|
chain: providerCode === "usdt" ? stringValue(form.chain).toUpperCase() : "",
|
||||||
coinAmount: numberValue(form.coinAmount),
|
coinAmount: numberValue(form.coinAmount),
|
||||||
externalOrderNo: stringValue(form.externalOrderNo),
|
externalOrderNo: stringValue(form.externalOrderNo),
|
||||||
|
providerAmountMinor: requiresThirdPartyProviderAmount(providerCode) ? providerAmountMinor : "",
|
||||||
providerCode,
|
providerCode,
|
||||||
|
providerCurrencyCode: requiresThirdPartyProviderAmount(providerCode) ? providerCurrencyCode : "",
|
||||||
remark: stringValue(form.remark),
|
remark: stringValue(form.remark),
|
||||||
targetUserId: stringValue(form.targetUserId),
|
targetUserId: stringValue(form.targetUserId),
|
||||||
usdAmount: numberValue(form.usdAmount),
|
usdAmount: numberValue(form.usdAmount),
|
||||||
@ -145,12 +149,16 @@ export function buildCoinSellerRechargeOrderPayload(form) {
|
|||||||
|
|
||||||
export function buildCoinSellerRechargeReceiptPayload(form) {
|
export function buildCoinSellerRechargeReceiptPayload(form) {
|
||||||
const providerCode = stringValue(form.providerCode);
|
const providerCode = stringValue(form.providerCode);
|
||||||
|
const providerCurrencyCode = stringValue(form.providerCurrencyCode).toUpperCase();
|
||||||
|
const providerAmountMinor = providerAmountMinorFromForm(form);
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries({
|
Object.entries({
|
||||||
appCode: stringValue(form.appCode),
|
appCode: stringValue(form.appCode),
|
||||||
chain: providerCode === "usdt" ? stringValue(form.chain).toUpperCase() : "",
|
chain: providerCode === "usdt" ? stringValue(form.chain).toUpperCase() : "",
|
||||||
externalOrderNo: stringValue(form.externalOrderNo),
|
externalOrderNo: stringValue(form.externalOrderNo),
|
||||||
|
providerAmountMinor: requiresThirdPartyProviderAmount(providerCode) ? providerAmountMinor : "",
|
||||||
providerCode,
|
providerCode,
|
||||||
|
providerCurrencyCode: requiresThirdPartyProviderAmount(providerCode) ? providerCurrencyCode : "",
|
||||||
usdAmount: numberValue(form.usdAmount),
|
usdAmount: numberValue(form.usdAmount),
|
||||||
}).filter(([, value]) => value !== ""),
|
}).filter(([, value]) => value !== ""),
|
||||||
);
|
);
|
||||||
@ -178,6 +186,14 @@ export function validateCoinSellerRechargeOrderPayload(payload) {
|
|||||||
if (payload.providerCode === "usdt" && !payload.chain) {
|
if (payload.providerCode === "usdt" && !payload.chain) {
|
||||||
return "请选择 USDT 链";
|
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) {
|
if (payload.remark && payload.remark.length > 512) {
|
||||||
return "备注不能超过 512 字";
|
return "备注不能超过 512 字";
|
||||||
}
|
}
|
||||||
@ -642,6 +658,26 @@ function numberOrNull(value) {
|
|||||||
return Number.isFinite(nextValue) ? nextValue : null;
|
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) {
|
function stringValue(value) {
|
||||||
return String(value ?? "").trim();
|
return String(value ?? "").trim();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -170,7 +170,9 @@ test("builds validates and normalizes coin seller recharge orders", () => {
|
|||||||
appCode: " lalu ",
|
appCode: " lalu ",
|
||||||
coinAmount: "100000",
|
coinAmount: "100000",
|
||||||
externalOrderNo: " MIFA-001 ",
|
externalOrderNo: " MIFA-001 ",
|
||||||
|
providerAmount: "1250.5",
|
||||||
providerCode: "mifapay",
|
providerCode: "mifapay",
|
||||||
|
providerCurrencyCode: " php ",
|
||||||
remark: " 人工补单 ",
|
remark: " 人工补单 ",
|
||||||
targetUserId: " 10001 ",
|
targetUserId: " 10001 ",
|
||||||
usdAmount: "10.5",
|
usdAmount: "10.5",
|
||||||
@ -180,7 +182,9 @@ test("builds validates and normalizes coin seller recharge orders", () => {
|
|||||||
appCode: "lalu",
|
appCode: "lalu",
|
||||||
coinAmount: 100000,
|
coinAmount: 100000,
|
||||||
externalOrderNo: "MIFA-001",
|
externalOrderNo: "MIFA-001",
|
||||||
|
providerAmountMinor: 125050,
|
||||||
providerCode: "mifapay",
|
providerCode: "mifapay",
|
||||||
|
providerCurrencyCode: "PHP",
|
||||||
remark: "人工补单",
|
remark: "人工补单",
|
||||||
targetUserId: "10001",
|
targetUserId: "10001",
|
||||||
usdAmount: 10.5,
|
usdAmount: 10.5,
|
||||||
@ -194,16 +198,90 @@ test("builds validates and normalizes coin seller recharge orders", () => {
|
|||||||
).toEqual({
|
).toEqual({
|
||||||
appCode: "lalu",
|
appCode: "lalu",
|
||||||
externalOrderNo: "MIFA-001",
|
externalOrderNo: "MIFA-001",
|
||||||
|
providerAmountMinor: 125050,
|
||||||
providerCode: "mifapay",
|
providerCode: "mifapay",
|
||||||
|
providerCurrencyCode: "PHP",
|
||||||
usdAmount: 10.5,
|
usdAmount: 10.5,
|
||||||
});
|
});
|
||||||
expect(validateCoinSellerRechargeOrderPayload(payload)).toBe("");
|
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: 0 })).toBe("");
|
||||||
expect(validateCoinSellerRechargeOrderPayload({ ...payload, coinAmount: -1 })).toBe("请填写不小于 0 的整数金币");
|
expect(validateCoinSellerRechargeOrderPayload({ ...payload, coinAmount: -1 })).toBe("请填写不小于 0 的整数金币");
|
||||||
expect(validateCoinSellerRechargeOrderPayload({ ...payload, remark: "x".repeat(513) })).toBe("备注不能超过 512 字");
|
expect(validateCoinSellerRechargeOrderPayload({ ...payload, remark: "x".repeat(513) })).toBe("备注不能超过 512 字");
|
||||||
expect(validateCoinSellerRechargeOrderPayload({ ...payload, providerCode: "usdt", chain: "" })).toBe(
|
expect(validateCoinSellerRechargeOrderPayload({ ...payload, providerCode: "usdt", chain: "" })).toBe(
|
||||||
"请选择 USDT 链",
|
"请选择 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({
|
const page = normalizeCoinSellerRechargeOrderPage({
|
||||||
items: [
|
items: [
|
||||||
|
|||||||
@ -134,6 +134,7 @@ export const fallbackNavigation = [
|
|||||||
routeNavItem("host-agency-policy", { icon: WalletOutlined }),
|
routeNavItem("host-agency-policy", { icon: WalletOutlined }),
|
||||||
routeNavItem("host-salary-settlement", { icon: ReceiptLongOutlined }),
|
routeNavItem("host-salary-settlement", { icon: ReceiptLongOutlined }),
|
||||||
routeNavItem("host-org-coin-sellers", { icon: WalletOutlined }),
|
routeNavItem("host-org-coin-sellers", { icon: WalletOutlined }),
|
||||||
|
routeNavItem("host-org-coin-seller-sub-applications", { icon: ReceiptLongOutlined }),
|
||||||
routeNavItem("host-withdrawals", { icon: ReceiptLongOutlined }),
|
routeNavItem("host-withdrawals", { icon: ReceiptLongOutlined }),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -43,6 +43,8 @@ export const PERMISSIONS = {
|
|||||||
coinSellerUpdate: "coin-seller:update",
|
coinSellerUpdate: "coin-seller:update",
|
||||||
coinSellerStockCredit: "coin-seller:stock-credit",
|
coinSellerStockCredit: "coin-seller:stock-credit",
|
||||||
coinSellerExchangeRate: "coin-seller:exchange-rate",
|
coinSellerExchangeRate: "coin-seller:exchange-rate",
|
||||||
|
coinSellerSubApplicationView: "coin-seller-sub-application:view",
|
||||||
|
coinSellerSubApplicationAudit: "coin-seller-sub-application:audit",
|
||||||
hostWithdrawalView: "host-withdrawal:view",
|
hostWithdrawalView: "host-withdrawal:view",
|
||||||
financeView: "finance:view",
|
financeView: "finance:view",
|
||||||
financeApplicationCreate: "finance-application:create",
|
financeApplicationCreate: "finance-application:create",
|
||||||
@ -286,6 +288,7 @@ export const MENU_CODES = {
|
|||||||
hostAgencyPolicy: "host-agency-policy",
|
hostAgencyPolicy: "host-agency-policy",
|
||||||
hostSalarySettlement: "host-salary-settlement",
|
hostSalarySettlement: "host-salary-settlement",
|
||||||
hostOrgCoinSellers: "host-org-coin-sellers",
|
hostOrgCoinSellers: "host-org-coin-sellers",
|
||||||
|
hostOrgCoinSellerSubApplications: "host-org-coin-seller-sub-applications",
|
||||||
hostWithdrawals: "host-withdrawals",
|
hostWithdrawals: "host-withdrawals",
|
||||||
payment: "payment",
|
payment: "payment",
|
||||||
paymentBillList: "payment-bill-list",
|
paymentBillList: "payment-bill-list",
|
||||||
|
|||||||
@ -13,6 +13,8 @@ import {
|
|||||||
disableRegion,
|
disableRegion,
|
||||||
listAgencies,
|
listAgencies,
|
||||||
listBDs,
|
listBDs,
|
||||||
|
listCoinSellerSubApplications,
|
||||||
|
listCoinSellerSubSellers,
|
||||||
listCoinSellers,
|
listCoinSellers,
|
||||||
listCountries,
|
listCountries,
|
||||||
listHosts,
|
listHosts,
|
||||||
@ -21,6 +23,8 @@ import {
|
|||||||
getJob,
|
getJob,
|
||||||
renameCountryCode,
|
renameCountryCode,
|
||||||
replaceRegionCountries,
|
replaceRegionCountries,
|
||||||
|
approveCoinSellerSubApplication,
|
||||||
|
rejectCoinSellerSubApplication,
|
||||||
setCoinSellerStatus,
|
setCoinSellerStatus,
|
||||||
updateBDLeaderPositionAlias,
|
updateBDLeaderPositionAlias,
|
||||||
updateCountry,
|
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" });
|
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 () => {
|
test("country and region APIs use generated admin paths", async () => {
|
||||||
vi.stubGlobal(
|
vi.stubGlobal(
|
||||||
"fetch",
|
"fetch",
|
||||||
|
|||||||
@ -15,6 +15,9 @@ import type {
|
|||||||
CoinSellerDto,
|
CoinSellerDto,
|
||||||
CoinSellerSalaryRatesDto,
|
CoinSellerSalaryRatesDto,
|
||||||
CoinSellerSalaryRatesPayload,
|
CoinSellerSalaryRatesPayload,
|
||||||
|
CoinSellerSubApplicationDto,
|
||||||
|
CoinSellerSubApplicationReviewPayload,
|
||||||
|
CoinSellerSubApplicationReviewResultDto,
|
||||||
CoinSellerStockCreditDto,
|
CoinSellerStockCreditDto,
|
||||||
CoinSellerStockCreditPayload,
|
CoinSellerStockCreditPayload,
|
||||||
CoinSellerStockDebitDto,
|
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>> {
|
export function listHostWithdrawals(query: PageQuery = {}): Promise<ApiPage<HostWithdrawalDto>> {
|
||||||
const endpoint = API_ENDPOINTS.listHostWithdrawals;
|
const endpoint = API_ENDPOINTS.listHostWithdrawals;
|
||||||
return apiRequest<ApiPage<HostWithdrawalDto>>(apiEndpointPath(API_OPERATIONS.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(
|
export function creditCoinSellerStock(
|
||||||
userId: EntityId,
|
userId: EntityId,
|
||||||
payload: CoinSellerStockCreditPayload,
|
payload: CoinSellerStockCreditPayload,
|
||||||
|
|||||||
@ -13,10 +13,13 @@ export function HostOrgStatus({ value }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getStatusTone(status) {
|
function getStatusTone(status) {
|
||||||
if (status === "active") {
|
if (status === "active" || status === "approved") {
|
||||||
return "running";
|
return "running";
|
||||||
}
|
}
|
||||||
if (status === "closed" || status === "disabled") {
|
if (status === "pending") {
|
||||||
|
return "warning";
|
||||||
|
}
|
||||||
|
if (status === "closed" || status === "disabled" || status === "rejected") {
|
||||||
return "danger";
|
return "danger";
|
||||||
}
|
}
|
||||||
return "stopped";
|
return "stopped";
|
||||||
|
|||||||
@ -1,53 +1,69 @@
|
|||||||
export const agencyStatusFilters = [
|
export const agencyStatusFilters = [
|
||||||
["", "全部"],
|
["", "全部"],
|
||||||
["active", "启用"],
|
["active", "启用"],
|
||||||
["closed", "已关闭"]
|
["closed", "已关闭"],
|
||||||
];
|
];
|
||||||
|
|
||||||
export const countryEnabledFilters = [
|
export const countryEnabledFilters = [
|
||||||
["", "全部"],
|
["", "全部"],
|
||||||
["enabled", "启用"],
|
["enabled", "启用"],
|
||||||
["disabled", "停用"]
|
["disabled", "停用"],
|
||||||
];
|
];
|
||||||
|
|
||||||
export const regionStatusFilters = [
|
export const regionStatusFilters = [
|
||||||
["", "全部"],
|
["", "全部"],
|
||||||
["active", "启用"],
|
["active", "启用"],
|
||||||
["disabled", "停用"]
|
["disabled", "停用"],
|
||||||
];
|
];
|
||||||
|
|
||||||
export const bdStatusFilters = [
|
export const bdStatusFilters = [
|
||||||
["", "全部"],
|
["", "全部"],
|
||||||
["active", "启用"],
|
["active", "启用"],
|
||||||
["disabled", "停用"]
|
["disabled", "停用"],
|
||||||
];
|
];
|
||||||
|
|
||||||
export const managerStatusFilters = [
|
export const managerStatusFilters = [
|
||||||
["", "全部"],
|
["", "全部"],
|
||||||
["active", "启用"],
|
["active", "启用"],
|
||||||
["disabled", "停用"]
|
["disabled", "停用"],
|
||||||
];
|
];
|
||||||
|
|
||||||
export const hostStatusFilters = [
|
export const hostStatusFilters = [
|
||||||
["", "全部"],
|
["", "全部"],
|
||||||
["active", "启用"],
|
["active", "启用"],
|
||||||
["disabled", "停用"]
|
["disabled", "停用"],
|
||||||
];
|
];
|
||||||
|
|
||||||
export const coinSellerStatusFilters = [
|
export const coinSellerStatusFilters = [
|
||||||
["", "全部"],
|
["", "全部"],
|
||||||
["active", "启用"],
|
["active", "启用"],
|
||||||
["disabled", "停用"]
|
["disabled", "停用"],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const coinSellerSubApplicationStatusFilters = [
|
||||||
|
["", "全部"],
|
||||||
|
["pending", "待审核"],
|
||||||
|
["approved", "已通过"],
|
||||||
|
["rejected", "已拒绝"],
|
||||||
|
];
|
||||||
|
|
||||||
|
export const coinSellerSubApplicationStatusLabels = {
|
||||||
|
approved: "已通过",
|
||||||
|
pending: "待审核",
|
||||||
|
rejected: "已拒绝",
|
||||||
|
};
|
||||||
|
|
||||||
export const hostSourceLabels = {
|
export const hostSourceLabels = {
|
||||||
agency_application: "Agency 申请",
|
agency_application: "Agency 申请",
|
||||||
agency_invitation: "Agency 邀请",
|
agency_invitation: "Agency 邀请",
|
||||||
admin_create_agency: "后台创建 Agency"
|
admin_create_agency: "后台创建 Agency",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const statusLabels = {
|
export const statusLabels = {
|
||||||
active: "启用",
|
active: "启用",
|
||||||
closed: "已关闭",
|
approved: "已通过",
|
||||||
disabled: "停用"
|
closed: "已关闭",
|
||||||
|
disabled: "停用",
|
||||||
|
pending: "待审核",
|
||||||
|
rejected: "已拒绝",
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
debitCoinSellerStock,
|
debitCoinSellerStock,
|
||||||
getCoinSellerSalaryRates,
|
getCoinSellerSalaryRates,
|
||||||
listCoinSellers,
|
listCoinSellers,
|
||||||
|
listCoinSellerSubSellers,
|
||||||
replaceCoinSellerSalaryRates,
|
replaceCoinSellerSalaryRates,
|
||||||
setCoinSellerStatus,
|
setCoinSellerStatus,
|
||||||
} from "@/features/host-org/api";
|
} from "@/features/host-org/api";
|
||||||
@ -79,6 +80,9 @@ export function useHostCoinSellersPage() {
|
|||||||
const [loadingRates, setLoadingRates] = useState(false);
|
const [loadingRates, setLoadingRates] = useState(false);
|
||||||
const [selectedSeller, setSelectedSeller] = useState(null);
|
const [selectedSeller, setSelectedSeller] = useState(null);
|
||||||
const [loadingAction, setLoadingAction] = useState("");
|
const [loadingAction, setLoadingAction] = useState("");
|
||||||
|
const [loadingSubSellers, setLoadingSubSellers] = useState(false);
|
||||||
|
const [subSellerData, setSubSellerData] = useState(emptyData);
|
||||||
|
const [subSellerPage, setSubSellerPageState] = useState(1);
|
||||||
const [contactSavingIds, setContactSavingIds] = useState({});
|
const [contactSavingIds, setContactSavingIds] = useState({});
|
||||||
const [sellerRowPatches, setSellerRowPatches] = useState({});
|
const [sellerRowPatches, setSellerRowPatches] = useState({});
|
||||||
const filters = useMemo(
|
const filters = useMemo(
|
||||||
@ -193,6 +197,15 @@ export function useHostCoinSellersPage() {
|
|||||||
setActiveAction("ledger");
|
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 () => {
|
const openRateSettings = async () => {
|
||||||
if (!abilities.canExchangeRate) {
|
if (!abilities.canExchangeRate) {
|
||||||
return;
|
return;
|
||||||
@ -212,6 +225,31 @@ export function useHostCoinSellersPage() {
|
|||||||
setSelectedSeller(null);
|
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) => {
|
const changeRateRegionId = async (value) => {
|
||||||
setRateRegionId(value);
|
setRateRegionId(value);
|
||||||
if (value) {
|
if (value) {
|
||||||
@ -453,6 +491,7 @@ export function useHostCoinSellersPage() {
|
|||||||
error,
|
error,
|
||||||
loading,
|
loading,
|
||||||
loadingAction,
|
loadingAction,
|
||||||
|
loadingSubSellers,
|
||||||
loadingRates,
|
loadingRates,
|
||||||
loadingRegions,
|
loadingRegions,
|
||||||
openRateSettings,
|
openRateSettings,
|
||||||
@ -461,6 +500,7 @@ export function useHostCoinSellersPage() {
|
|||||||
openSellerLedger,
|
openSellerLedger,
|
||||||
openStockCredit,
|
openStockCredit,
|
||||||
openStockDebit,
|
openStockDebit,
|
||||||
|
openSubSellerList,
|
||||||
page,
|
page,
|
||||||
regionId,
|
regionId,
|
||||||
regionOptions,
|
regionOptions,
|
||||||
@ -478,9 +518,12 @@ export function useHostCoinSellersPage() {
|
|||||||
setPage,
|
setPage,
|
||||||
setStockForm,
|
setStockForm,
|
||||||
setStockDebitForm,
|
setStockDebitForm,
|
||||||
|
setSubSellerPage,
|
||||||
status,
|
status,
|
||||||
stockDebitForm,
|
stockDebitForm,
|
||||||
stockForm,
|
stockForm,
|
||||||
|
subSellerData,
|
||||||
|
subSellerPage,
|
||||||
sortBy,
|
sortBy,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
submitRateSettings,
|
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() {
|
export function HostCoinSellersPage() {
|
||||||
const page = useHostCoinSellersPage();
|
const page = useHostCoinSellersPage();
|
||||||
const items = page.data.items || [];
|
const items = page.data.items || [];
|
||||||
@ -99,6 +132,12 @@ export function HostCoinSellersPage() {
|
|||||||
render: (item) => <SubCoinSellerPermissionSwitch item={item} page={page} />,
|
render: (item) => <SubCoinSellerPermissionSwitch item={item} page={page} />,
|
||||||
width: "minmax(120px, 0.7fr)",
|
width: "minmax(120px, 0.7fr)",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "subCoinSellerCount",
|
||||||
|
label: "子币商数量",
|
||||||
|
render: (item) => <SubCoinSellerCountButton item={item} page={page} />,
|
||||||
|
width: "minmax(120px, 0.7fr)",
|
||||||
|
},
|
||||||
...baseColumns.slice(2),
|
...baseColumns.slice(2),
|
||||||
page.abilities.canUpdate || page.abilities.canStockCredit || page.abilities.canLedger
|
page.abilities.canUpdate || page.abilities.canStockCredit || page.abilities.canLedger
|
||||||
? {
|
? {
|
||||||
@ -220,9 +259,7 @@ export function HostCoinSellersPage() {
|
|||||||
checked={page.createForm.canManageSubCoinSellers !== false}
|
checked={page.createForm.canManageSubCoinSellers !== false}
|
||||||
disabled={createDisabled}
|
disabled={createDisabled}
|
||||||
label="子币商权限"
|
label="子币商权限"
|
||||||
onChange={(checked) =>
|
onChange={(checked) => page.setCreateForm({ ...page.createForm, canManageSubCoinSellers: checked })}
|
||||||
page.setCreateForm({ ...page.createForm, canManageSubCoinSellers: checked })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</HostOrgActionModal>
|
</HostOrgActionModal>
|
||||||
|
|
||||||
@ -380,7 +417,9 @@ export function HostCoinSellersPage() {
|
|||||||
required
|
required
|
||||||
minRows={2}
|
minRows={2}
|
||||||
value={stockDebitForm.reason}
|
value={stockDebitForm.reason}
|
||||||
onChange={(event) => page.setStockDebitForm?.({ ...stockDebitForm, reason: event.target.value })}
|
onChange={(event) =>
|
||||||
|
page.setStockDebitForm?.({ ...stockDebitForm, reason: event.target.value })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</AdminFormSection>
|
</AdminFormSection>
|
||||||
</HostOrgActionModal>
|
</HostOrgActionModal>
|
||||||
@ -401,6 +440,35 @@ export function HostCoinSellersPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
</SideDrawer>
|
</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
|
<SideDrawer
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
@ -509,9 +577,7 @@ function SubCoinSellerPermissionSwitch({ item, page }) {
|
|||||||
<AdminSwitch
|
<AdminSwitch
|
||||||
checked={item.canManageSubCoinSellers === true}
|
checked={item.canManageSubCoinSellers === true}
|
||||||
checkedLabel="开"
|
checkedLabel="开"
|
||||||
disabled={
|
disabled={!page.abilities.canUpdate || page.loadingAction === `coin-seller-sub-permission-${item.userId}`}
|
||||||
!page.abilities.canUpdate || page.loadingAction === `coin-seller-sub-permission-${item.userId}`
|
|
||||||
}
|
|
||||||
inputProps={{ "aria-label": item.canManageSubCoinSellers === true ? "关闭子币商权限" : "开启子币商权限" }}
|
inputProps={{ "aria-label": item.canManageSubCoinSellers === true ? "关闭子币商权限" : "开启子币商权限" }}
|
||||||
uncheckedLabel="关"
|
uncheckedLabel="关"
|
||||||
onChange={(event) => page.toggleSubCoinSellerPermission(item, event.target.checked)}
|
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 }) {
|
function SellerContact({ item, page }) {
|
||||||
const contact = String(item.contact || "").trim();
|
const contact = String(item.contact || "").trim();
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { useHostCoinSellersPage } from "@/features/host-org/hooks/useHostCoinSel
|
|||||||
const mocks = vi.hoisted(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
ledgerTable: vi.fn(),
|
ledgerTable: vi.fn(),
|
||||||
openSellerLedger: vi.fn(),
|
openSellerLedger: vi.fn(),
|
||||||
|
openSubSellerList: vi.fn(),
|
||||||
toggleSubCoinSellerPermission: 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.queryByText("创建人 ID")).not.toBeInTheDocument();
|
||||||
expect(screen.getByText("累充USDT")).toBeInTheDocument();
|
expect(screen.getByText("累充USDT")).toBeInTheDocument();
|
||||||
expect(screen.getByText("子币商权限")).toBeInTheDocument();
|
expect(screen.getByText("子币商权限")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("子币商数量")).toBeInTheDocument();
|
||||||
expect(screen.getByText("12.5")).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", () => {
|
test("coin seller sub permission switch toggles through the page hook", () => {
|
||||||
const seller = sellerFixture();
|
const seller = sellerFixture();
|
||||||
vi.mocked(useHostCoinSellersPage).mockReturnValue(
|
vi.mocked(useHostCoinSellersPage).mockReturnValue(
|
||||||
@ -104,7 +121,34 @@ test("coin seller ledger drawer passes locked seller id to shared table", () =>
|
|||||||
render(<HostCoinSellersPage />);
|
render(<HostCoinSellersPage />);
|
||||||
|
|
||||||
expect(screen.getByTestId("coin-seller-ledger-table")).toHaveTextContent("3001");
|
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", () => {
|
test("coin seller create form includes sub permission switch", () => {
|
||||||
@ -189,6 +233,7 @@ function pageFixture(patch = {}) {
|
|||||||
openRateSettings: vi.fn(),
|
openRateSettings: vi.fn(),
|
||||||
openSellerLedger: mocks.openSellerLedger,
|
openSellerLedger: mocks.openSellerLedger,
|
||||||
openStockCredit: vi.fn(),
|
openStockCredit: vi.fn(),
|
||||||
|
openSubSellerList: mocks.openSubSellerList,
|
||||||
page: 1,
|
page: 1,
|
||||||
query: "",
|
query: "",
|
||||||
rateRegionId: "",
|
rateRegionId: "",
|
||||||
@ -203,8 +248,11 @@ function pageFixture(patch = {}) {
|
|||||||
setCreateForm: vi.fn(),
|
setCreateForm: vi.fn(),
|
||||||
setEditForm: vi.fn(),
|
setEditForm: vi.fn(),
|
||||||
setPage: vi.fn(),
|
setPage: vi.fn(),
|
||||||
|
setSubSellerPage: vi.fn(),
|
||||||
setStockForm: vi.fn(),
|
setStockForm: vi.fn(),
|
||||||
status: "",
|
status: "",
|
||||||
|
subSellerData: { items: [], page: 1, pageSize: 50, total: 0 },
|
||||||
|
subSellerPage: 1,
|
||||||
stockDebitForm: { coinAmount: "", reason: "", rechargeAmount: "" },
|
stockDebitForm: { coinAmount: "", reason: "", rechargeAmount: "" },
|
||||||
stockForm: { coinAmount: "", reason: "", rechargeAmount: "", type: "usdt_purchase" },
|
stockForm: { coinAmount: "", reason: "", rechargeAmount: "", type: "usdt_purchase" },
|
||||||
sortBy: "",
|
sortBy: "",
|
||||||
@ -220,7 +268,7 @@ function pageFixture(patch = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function sellerFixture() {
|
function sellerFixture(patch = {}) {
|
||||||
return {
|
return {
|
||||||
avatar: "",
|
avatar: "",
|
||||||
canManageSubCoinSellers: true,
|
canManageSubCoinSellers: true,
|
||||||
@ -230,9 +278,11 @@ function sellerFixture() {
|
|||||||
merchantBalance: 9400000,
|
merchantBalance: 9400000,
|
||||||
regionId: 1,
|
regionId: 1,
|
||||||
status: "active",
|
status: "active",
|
||||||
|
subCoinSellerCount: 2,
|
||||||
totalRechargeUsdtMicro: 12500000,
|
totalRechargeUsdtMicro: 12500000,
|
||||||
updatedAtMs: 1760000000000,
|
updatedAtMs: 1760000000000,
|
||||||
userId: "3001",
|
userId: "3001",
|
||||||
username: "Flower92",
|
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() {
|
export function useHostWithdrawalAbilities() {
|
||||||
const { can } = useAuth();
|
const { can } = useAuth();
|
||||||
|
|
||||||
|
|||||||
@ -65,6 +65,17 @@ export const hostOrgRoutes = [
|
|||||||
path: "/host/coin-sellers",
|
path: "/host/coin-sellers",
|
||||||
permission: PERMISSIONS.coinSellerView,
|
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: "主播提现",
|
label: "主播提现",
|
||||||
loader: () => import("./pages/HostWithdrawalsPage.jsx").then((module) => module.HostWithdrawalsPage),
|
loader: () => import("./pages/HostWithdrawalsPage.jsx").then((module) => module.HostWithdrawalsPage),
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import {
|
|||||||
agencyCloseSchema,
|
agencyCloseSchema,
|
||||||
agencyJoinEnabledSchema,
|
agencyJoinEnabledSchema,
|
||||||
coinSellerStatusSchema,
|
coinSellerStatusSchema,
|
||||||
|
coinSellerSubApplicationReviewSchema,
|
||||||
createCoinSellerSchema,
|
createCoinSellerSchema,
|
||||||
createManagerSchema,
|
createManagerSchema,
|
||||||
countryRenameCodeSchema,
|
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", () => {
|
test("agency action schemas preserve large agency ids as strings", () => {
|
||||||
const agencyId = "321170072154411009";
|
const agencyId = "321170072154411009";
|
||||||
const closeResult = agencyCloseSchema.safeParse({
|
const closeResult = agencyCloseSchema.safeParse({
|
||||||
|
|||||||
@ -130,6 +130,8 @@ export const coinSellerStatusSchema = commandContactSchema.extend({
|
|||||||
targetUserId: entityIdSchema,
|
targetUserId: entityIdSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const coinSellerSubApplicationReviewSchema = commandBaseSchema;
|
||||||
|
|
||||||
export const coinSellerStockCreditSchema = z
|
export const coinSellerStockCreditSchema = z
|
||||||
.object({
|
.object({
|
||||||
coinAmount: z.coerce.number().int().positive("请输入充值金币"),
|
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 BDStatusForm = z.infer<typeof bdStatusSchema>;
|
||||||
export type CreateCoinSellerForm = z.infer<typeof createCoinSellerSchema>;
|
export type CreateCoinSellerForm = z.infer<typeof createCoinSellerSchema>;
|
||||||
export type CoinSellerStatusForm = z.infer<typeof coinSellerStatusSchema>;
|
export type CoinSellerStatusForm = z.infer<typeof coinSellerStatusSchema>;
|
||||||
|
export type CoinSellerSubApplicationReviewForm = z.infer<typeof coinSellerSubApplicationReviewSchema>;
|
||||||
export type CoinSellerStockCreditForm = z.infer<typeof coinSellerStockCreditSchema>;
|
export type CoinSellerStockCreditForm = z.infer<typeof coinSellerStockCreditSchema>;
|
||||||
export type CoinSellerStockDebitForm = z.infer<typeof coinSellerStockDebitSchema>;
|
export type CoinSellerStockDebitForm = z.infer<typeof coinSellerStockDebitSchema>;
|
||||||
export type CreateAgencyForm = z.infer<typeof createAgencySchema>;
|
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"];
|
patch: operations["setCoinSellerStatus"];
|
||||||
trace?: never;
|
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": {
|
"/admin/coin-sellers/{user_id}/stock-credits": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@ -4096,6 +4160,12 @@ export interface components {
|
|||||||
pageSize: number;
|
pageSize: number;
|
||||||
total: number;
|
total: number;
|
||||||
};
|
};
|
||||||
|
ApiPageCoinSellerSubApplication: {
|
||||||
|
items: components["schemas"]["CoinSellerSubApplication"][];
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
ApiPageCoinSellerLedger: {
|
ApiPageCoinSellerLedger: {
|
||||||
items: components["schemas"]["CoinSellerLedger"][];
|
items: components["schemas"]["CoinSellerLedger"][];
|
||||||
page: number;
|
page: number;
|
||||||
@ -4255,6 +4325,12 @@ export interface components {
|
|||||||
ApiResponseCoinSeller: components["schemas"]["Envelope"] & {
|
ApiResponseCoinSeller: components["schemas"]["Envelope"] & {
|
||||||
data?: components["schemas"]["CoinSeller"];
|
data?: components["schemas"]["CoinSeller"];
|
||||||
};
|
};
|
||||||
|
ApiResponseCoinSellerSubApplicationPage: components["schemas"]["Envelope"] & {
|
||||||
|
data?: components["schemas"]["ApiPageCoinSellerSubApplication"];
|
||||||
|
};
|
||||||
|
ApiResponseCoinSellerSubApplicationReview: components["schemas"]["Envelope"] & {
|
||||||
|
data?: components["schemas"]["CoinSellerSubApplicationReviewResult"];
|
||||||
|
};
|
||||||
ApiResponseCoinSellerLedgerPage: components["schemas"]["Envelope"] & {
|
ApiResponseCoinSellerLedgerPage: components["schemas"]["Envelope"] & {
|
||||||
data?: components["schemas"]["ApiPageCoinSellerLedger"];
|
data?: components["schemas"]["ApiPageCoinSellerLedger"];
|
||||||
};
|
};
|
||||||
@ -4492,11 +4568,79 @@ export interface components {
|
|||||||
regionName?: string;
|
regionName?: string;
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
status?: "active" | "disabled";
|
status?: "active" | "disabled";
|
||||||
|
subCoinSellerCount?: number;
|
||||||
totalRechargeUsdtMicro?: number;
|
totalRechargeUsdtMicro?: number;
|
||||||
updatedAtMs?: number;
|
updatedAtMs?: number;
|
||||||
userId: string;
|
userId: string;
|
||||||
username?: 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: {
|
CoinLedgerUser: {
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
displayUserId?: string;
|
displayUserId?: string;
|
||||||
@ -4509,7 +4653,15 @@ export interface components {
|
|||||||
/** Format: int64 */
|
/** Format: int64 */
|
||||||
availableDelta: number;
|
availableDelta: number;
|
||||||
/** @enum {string} */
|
/** @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;
|
commandId?: string;
|
||||||
counterpartyUserId?: string;
|
counterpartyUserId?: string;
|
||||||
/** Format: int64 */
|
/** Format: int64 */
|
||||||
@ -4841,7 +4993,13 @@ export interface components {
|
|||||||
appCode: string;
|
appCode: string;
|
||||||
appName?: string;
|
appName?: string;
|
||||||
/** @enum {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;
|
walletIdentity?: string;
|
||||||
targetUserId: string;
|
targetUserId: string;
|
||||||
coinAmount: number;
|
coinAmount: number;
|
||||||
@ -4923,7 +5081,13 @@ export interface components {
|
|||||||
FinanceApplicationInput: {
|
FinanceApplicationInput: {
|
||||||
appCode: string;
|
appCode: string;
|
||||||
/** @enum {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} */
|
/** @enum {string} */
|
||||||
walletIdentity?: "host" | "agency" | "bd";
|
walletIdentity?: "host" | "agency" | "bd";
|
||||||
targetUserId: string;
|
targetUserId: string;
|
||||||
@ -4940,6 +5104,8 @@ export interface components {
|
|||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
providerCode: "mifapay" | "v5pay" | "usdt";
|
providerCode: "mifapay" | "v5pay" | "usdt";
|
||||||
externalOrderNo: string;
|
externalOrderNo: string;
|
||||||
|
providerCurrencyCode?: string;
|
||||||
|
providerAmountMinor?: number;
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
chain?: "TRON" | "BSC";
|
chain?: "TRON" | "BSC";
|
||||||
remark?: string;
|
remark?: string;
|
||||||
@ -4968,6 +5134,8 @@ export interface components {
|
|||||||
chain?: "TRON" | "BSC";
|
chain?: "TRON" | "BSC";
|
||||||
usdAmount?: number;
|
usdAmount?: number;
|
||||||
usdMinorAmount?: number;
|
usdMinorAmount?: number;
|
||||||
|
providerCurrencyCode?: string;
|
||||||
|
providerAmountMinor?: number;
|
||||||
};
|
};
|
||||||
UserFinanceScope: {
|
UserFinanceScope: {
|
||||||
appCode: string;
|
appCode: string;
|
||||||
@ -5021,6 +5189,24 @@ export interface components {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description OK */
|
/** @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: {
|
CoinSellerLedgerPageResponse: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
@ -5398,6 +5584,11 @@ export interface components {
|
|||||||
"application/json": components["schemas"]["CoinSellerStatusPayload"];
|
"application/json": components["schemas"]["CoinSellerStatusPayload"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
CoinSellerSubApplicationReviewRequest: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["CoinSellerSubApplicationReviewPayload"];
|
||||||
|
};
|
||||||
|
};
|
||||||
CountryCreateRequest: {
|
CountryCreateRequest: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["CountryCreateInput"];
|
"application/json": components["schemas"]["CountryCreateInput"];
|
||||||
@ -6893,6 +7084,70 @@ export interface operations {
|
|||||||
200: components["responses"]["CoinSellerResponse"];
|
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: {
|
creditCoinSellerStock: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@ -7770,7 +8025,11 @@ export interface operations {
|
|||||||
seller_filter_user_id?: string;
|
seller_filter_user_id?: string;
|
||||||
seller_display_user_id?: string;
|
seller_display_user_id?: string;
|
||||||
seller_username?: 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;
|
start_at_ms?: number;
|
||||||
end_at_ms?: number;
|
end_at_ms?: number;
|
||||||
};
|
};
|
||||||
@ -7793,7 +8052,11 @@ export interface operations {
|
|||||||
seller_filter_user_id?: string;
|
seller_filter_user_id?: string;
|
||||||
seller_display_user_id?: string;
|
seller_display_user_id?: string;
|
||||||
seller_username?: 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;
|
start_at_ms?: number;
|
||||||
end_at_ms?: number;
|
end_at_ms?: number;
|
||||||
};
|
};
|
||||||
@ -7847,7 +8110,13 @@ export interface operations {
|
|||||||
sent_at_ms?: number;
|
sent_at_ms?: number;
|
||||||
summary: string;
|
summary: string;
|
||||||
/** @enum {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 */
|
/** Format: int64 */
|
||||||
target_user_id?: number;
|
target_user_id?: number;
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@ -628,7 +628,9 @@ export interface FinanceCoinSellerRechargeOrderPayload {
|
|||||||
chain?: string;
|
chain?: string;
|
||||||
coinAmount: number;
|
coinAmount: number;
|
||||||
externalOrderNo: string;
|
externalOrderNo: string;
|
||||||
|
providerAmountMinor?: number;
|
||||||
providerCode: "mifapay" | "v5pay" | "usdt";
|
providerCode: "mifapay" | "v5pay" | "usdt";
|
||||||
|
providerCurrencyCode?: string;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
targetUserId: EntityId;
|
targetUserId: EntityId;
|
||||||
usdAmount: number;
|
usdAmount: number;
|
||||||
@ -638,7 +640,9 @@ export interface FinanceCoinSellerRechargeReceiptVerificationPayload {
|
|||||||
appCode: string;
|
appCode: string;
|
||||||
chain?: string;
|
chain?: string;
|
||||||
externalOrderNo: string;
|
externalOrderNo: string;
|
||||||
|
providerAmountMinor?: number;
|
||||||
providerCode: "mifapay" | "v5pay" | "usdt";
|
providerCode: "mifapay" | "v5pay" | "usdt";
|
||||||
|
providerCurrencyCode?: string;
|
||||||
usdAmount?: number;
|
usdAmount?: number;
|
||||||
usdMinorAmount?: number;
|
usdMinorAmount?: number;
|
||||||
}
|
}
|
||||||
@ -842,12 +846,85 @@ export interface CoinSellerDto {
|
|||||||
regionId?: number;
|
regionId?: number;
|
||||||
regionName?: string;
|
regionName?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
|
subCoinSellerCount?: number;
|
||||||
totalRechargeUsdtMicro?: number;
|
totalRechargeUsdtMicro?: number;
|
||||||
updatedAtMs?: number;
|
updatedAtMs?: number;
|
||||||
userId: string;
|
userId: string;
|
||||||
username?: 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 {
|
export interface CoinSellerStockCreditPayload {
|
||||||
coinAmount: number;
|
coinAmount: number;
|
||||||
commandId: string;
|
commandId: string;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user