diff --git a/contracts/admin-openapi.json b/contracts/admin-openapi.json index c0ab697..cb603f1 100644 --- a/contracts/admin-openapi.json +++ b/contracts/admin-openapi.json @@ -3578,6 +3578,148 @@ "x-permissions": ["finance-withdrawal:audit", "finance-application:audit"] } }, + "/admin/finance/orders/coin-seller-recharges": { + "get": { + "operationId": "listFinanceCoinSellerRechargeOrders", + "parameters": [ + { + "in": "query", + "name": "app_code", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "provider_code", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "status", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "verify_status", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "grant_status", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "keyword", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "in": "query", + "name": "page_size", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/FinanceCoinSellerRechargeOrderPageResponse" + } + }, + "x-permission": "finance-order:coin-seller-recharge:view", + "x-permissions": ["finance-order:coin-seller-recharge:view"] + }, + "post": { + "operationId": "createFinanceCoinSellerRechargeOrder", + "requestBody": { + "$ref": "#/components/requestBodies/FinanceCoinSellerRechargeOrderRequest" + }, + "responses": { + "201": { + "$ref": "#/components/responses/FinanceCoinSellerRechargeOrderResponse" + } + }, + "x-permission": "finance-order:coin-seller-recharge:create", + "x-permissions": ["finance-order:coin-seller-recharge:create"] + } + }, + "/admin/finance/orders/coin-seller-recharges/receipt-verifications": { + "post": { + "operationId": "verifyFinanceCoinSellerRechargeReceipt", + "requestBody": { + "$ref": "#/components/requestBodies/FinanceCoinSellerRechargeReceiptVerificationRequest" + }, + "responses": { + "200": { + "$ref": "#/components/responses/FinanceCoinSellerRechargeReceiptVerificationResponse" + } + }, + "x-permission": "finance-order:coin-seller-recharge:verify", + "x-permissions": ["finance-order:coin-seller-recharge:verify"] + } + }, + "/admin/finance/orders/coin-seller-recharges/{order_id}/verify": { + "post": { + "operationId": "verifyFinanceCoinSellerRechargeOrder", + "parameters": [ + { + "in": "path", + "name": "order_id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/FinanceCoinSellerRechargeOrderResponse" + } + }, + "x-permission": "finance-order:coin-seller-recharge:verify", + "x-permissions": ["finance-order:coin-seller-recharge:verify"] + } + }, + "/admin/finance/orders/coin-seller-recharges/{order_id}/grant": { + "post": { + "operationId": "grantFinanceCoinSellerRechargeOrder", + "parameters": [ + { + "in": "path", + "name": "order_id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/FinanceCoinSellerRechargeOrderResponse" + } + }, + "x-permission": "finance-order:coin-seller-recharge:grant", + "x-permissions": ["finance-order:coin-seller-recharge:grant"] + } + }, "/admin/payment/temporary-links": { "get": { "operationId": "listTemporaryPaymentLinks", @@ -6073,6 +6215,24 @@ } } }, + "FinanceCoinSellerRechargeOrderRequest": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FinanceCoinSellerRechargeOrderInput" + } + } + } + }, + "FinanceCoinSellerRechargeReceiptVerificationRequest": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FinanceCoinSellerRechargeReceiptVerificationInput" + } + } + } + }, "FinanceApplicationAuditRequest": { "content": { "application/json": { @@ -6345,6 +6505,36 @@ } } }, + "FinanceCoinSellerRechargeOrderPageResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseFinanceCoinSellerRechargeOrderPage" + } + } + } + }, + "FinanceCoinSellerRechargeOrderResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseFinanceCoinSellerRechargeOrder" + } + } + } + }, + "FinanceCoinSellerRechargeReceiptVerificationResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseFinanceCoinSellerRechargeReceiptVerification" + } + } + } + }, "FinanceWithdrawalApplicationPageResponse": { "description": "OK", "content": { @@ -6694,6 +6884,27 @@ } } }, + "ApiPageFinanceCoinSellerRechargeOrder": { + "type": "object", + "required": ["items", "page", "pageSize", "total"], + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FinanceCoinSellerRechargeOrder" + } + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, "ApiPageFinanceWithdrawalApplication": { "type": "object", "required": ["items", "page", "pageSize", "total"], @@ -6896,6 +7107,51 @@ } ] }, + "ApiResponseFinanceCoinSellerRechargeOrder": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/FinanceCoinSellerRechargeOrder" + } + } + } + ] + }, + "ApiResponseFinanceCoinSellerRechargeReceiptVerification": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/FinanceCoinSellerRechargeReceiptVerification" + } + } + } + ] + }, + "ApiResponseFinanceCoinSellerRechargeOrderPage": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/ApiPageFinanceCoinSellerRechargeOrder" + } + } + } + ] + }, "ApiResponseFinanceWithdrawalApplication": { "allOf": [ { @@ -8076,6 +8332,133 @@ } } }, + "FinanceCoinSellerRechargeOrder": { + "type": "object", + "required": [ + "id", + "appCode", + "targetUserId", + "usdAmount", + "coinAmount", + "providerCode", + "externalOrderNo", + "status", + "verifyStatus", + "grantStatus", + "createdAtMs" + ], + "properties": { + "id": { + "type": "string" + }, + "appCode": { + "type": "string" + }, + "targetUserId": { + "type": "string" + }, + "targetDisplayUserId": { + "type": "string" + }, + "usdAmount": { + "type": "string" + }, + "usdMinorAmount": { + "type": "integer" + }, + "coinAmount": { + "type": "integer" + }, + "providerCode": { + "type": "string", + "enum": ["mifapay", "v5pay", "usdt"] + }, + "chain": { + "type": "string", + "enum": ["TRON", "BSC"] + }, + "externalOrderNo": { + "type": "string" + }, + "providerCountryCode": { + "type": "string" + }, + "providerCurrencyCode": { + "type": "string" + }, + "providerAmountMinor": { + "type": "integer" + }, + "providerOrderId": { + "type": "string" + }, + "providerStatus": { + "type": "string" + }, + "providerPayType": { + "type": "string" + }, + "receiveAddress": { + "type": "string" + }, + "orderDate": { + "type": "string" + }, + "status": { + "type": "string" + }, + "verifyStatus": { + "type": "string" + }, + "grantStatus": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "failureReason": { + "type": "string" + }, + "providerPayloadJson": { + "type": "string" + }, + "walletCommandId": { + "type": "string" + }, + "walletTransactionId": { + "type": "string" + }, + "walletAssetType": { + "type": "string" + }, + "walletAmountDelta": { + "type": "integer" + }, + "walletBalanceAfter": { + "type": "integer" + }, + "operatorName": { + "type": "string" + }, + "verifiedByName": { + "type": "string" + }, + "grantedByName": { + "type": "string" + }, + "createdAtMs": { + "type": "integer" + }, + "verifiedAtMs": { + "nullable": true, + "type": "integer" + }, + "grantedAtMs": { + "nullable": true, + "type": "integer" + } + } + }, "FinanceWithdrawalApplication": { "type": "object", "required": [ @@ -8190,6 +8573,112 @@ } } }, + "FinanceCoinSellerRechargeOrderInput": { + "type": "object", + "required": ["appCode", "targetUserId", "usdAmount", "coinAmount", "providerCode", "externalOrderNo"], + "properties": { + "appCode": { + "type": "string" + }, + "targetUserId": { + "type": "string" + }, + "usdAmount": { + "type": "number", + "minimum": 0 + }, + "coinAmount": { + "type": "integer", + "minimum": 0 + }, + "providerCode": { + "type": "string", + "enum": ["mifapay", "v5pay", "usdt"] + }, + "externalOrderNo": { + "type": "string" + }, + "chain": { + "type": "string", + "enum": ["TRON", "BSC"] + }, + "remark": { + "type": "string", + "maxLength": 512 + } + } + }, + "FinanceCoinSellerRechargeReceiptVerification": { + "type": "object", + "required": ["verified"], + "properties": { + "verified": { + "type": "boolean" + }, + "providerCode": { + "type": "string" + }, + "externalOrderNo": { + "type": "string" + }, + "providerOrderId": { + "type": "string" + }, + "status": { + "type": "string" + }, + "currencyCode": { + "type": "string" + }, + "providerAmountMinor": { + "type": "integer" + }, + "chain": { + "type": "string", + "enum": ["TRON", "BSC"] + }, + "receiveAddress": { + "type": "string" + }, + "verifiedAtMs": { + "type": "integer" + }, + "failureReason": { + "type": "string" + }, + "rawJson": { + "type": "string" + } + } + }, + "FinanceCoinSellerRechargeReceiptVerificationInput": { + "type": "object", + "required": ["appCode", "providerCode", "externalOrderNo"], + "properties": { + "appCode": { + "type": "string" + }, + "providerCode": { + "type": "string", + "enum": ["mifapay", "v5pay", "usdt"] + }, + "externalOrderNo": { + "type": "string" + }, + "chain": { + "type": "string", + "enum": ["TRON", "BSC"] + }, + "usdAmount": { + "type": "number", + "minimum": 0 + }, + "usdMinorAmount": { + "type": "integer", + "minimum": 0 + } + } + }, "UserFinanceScope": { "type": "object", "required": ["appCode", "regionId"], diff --git a/finance/src/FinanceApp.jsx b/finance/src/FinanceApp.jsx index 23d0a9a..8ed3f5e 100644 --- a/finance/src/FinanceApp.jsx +++ b/finance/src/FinanceApp.jsx @@ -6,6 +6,7 @@ import { approveFinanceApplication, approveFinanceWithdrawalApplication, createFinanceApplication, + createFinanceCoinSellerRechargeOrder, exportFinanceRechargeBills, fetchFinanceApplicationOptions, fetchFinanceRechargeApps, @@ -13,7 +14,9 @@ import { fetchFinanceRechargeSummaries, fetchFinanceSession, filterOperationsByPermissions, + grantFinanceCoinSellerRechargeOrder, listFinanceApplications, + listFinanceCoinSellerRechargeOrders, listFinanceRechargeDetails, listFinanceRechargeRegions, listMyFinanceApplications, @@ -21,10 +24,13 @@ import { refreshFinanceGoogleRechargePaid, rejectFinanceApplication, rejectFinanceWithdrawalApplication, + verifyFinanceCoinSellerRechargeReceipt, + verifyFinanceCoinSellerRechargeOrder, } from "./api.js"; import { FinanceApplicationForm } from "./components/FinanceApplicationForm.jsx"; import { FinanceApplicationList } from "./components/FinanceApplicationList.jsx"; import { FinanceAuditDialog } from "./components/FinanceAuditDialog.jsx"; +import { FinanceCoinSellerRechargeOrderList } from "./components/FinanceCoinSellerRechargeOrderList.jsx"; import { FinanceMyApplicationList } from "./components/FinanceMyApplicationList.jsx"; import { FinanceOverview } from "./components/FinanceOverview.jsx"; import { FinanceReconciliation } from "./components/FinanceReconciliation.jsx"; @@ -124,6 +130,12 @@ export function FinanceApp() { error: "", loading: false, }); + const [coinSellerRechargeOrdersState, setCoinSellerRechargeOrdersState] = useState({ + data: { items: [], page: 1, pageSize, total: 0 }, + error: "", + loading: false, + }); + const [coinSellerRechargeOrderActionLoading, setCoinSellerRechargeOrderActionLoading] = useState(""); const [activeView, setActiveView] = useState("create"); const [rechargeDetailFilters, setRechargeDetailFilters] = useState(() => ({ appCode: "", @@ -139,6 +151,15 @@ export function FinanceApp() { const [myFilters, setMyFilters] = useState({ appCode: "", keyword: "", operation: "", page: 1, status: "" }); const [filters, setFilters] = useState({ appCode: "", keyword: "", operation: "", page: 1, status: "pending" }); const [withdrawalFilters, setWithdrawalFilters] = useState({ appCode: "", keyword: "", page: 1 }); + const [coinSellerRechargeOrderFilters, setCoinSellerRechargeOrderFilters] = useState({ + appCode: "", + grantStatus: "", + keyword: "", + page: 1, + providerCode: "", + status: "", + verifyStatus: "", + }); const [form, setForm] = useState(DEFAULT_APPLICATION_FORM); const [createDialogOpen, setCreateDialogOpen] = useState(false); const [submitting, setSubmitting] = useState(false); @@ -158,11 +179,13 @@ export function FinanceApp() { setActiveView( session.canViewAppRechargeDetails ? "overview" - : session.canAuditApplication - ? "applications" - : session.canCreateApplication - ? "create" - : "withdrawals", + : session.canViewCoinSellerRechargeOrders + ? "coinSellerRechargeOrders" + : session.canAuditApplication + ? "applications" + : session.canCreateApplication + ? "create" + : "withdrawals", ); }) .catch((err) => { @@ -233,6 +256,19 @@ export function FinanceApp() { }), [withdrawalFilters], ); + const coinSellerRechargeOrderQuery = useMemo( + () => ({ + app_code: coinSellerRechargeOrderFilters.appCode, + grant_status: coinSellerRechargeOrderFilters.grantStatus, + keyword: coinSellerRechargeOrderFilters.keyword, + page: coinSellerRechargeOrderFilters.page, + page_size: pageSize, + provider_code: coinSellerRechargeOrderFilters.providerCode, + status: coinSellerRechargeOrderFilters.status, + verify_status: coinSellerRechargeOrderFilters.verifyStatus, + }), + [coinSellerRechargeOrderFilters], + ); const loadOptions = useCallback(async () => { if (!canLoadOptions) { @@ -464,6 +500,23 @@ export function FinanceApp() { } }, [session?.canViewWithdrawalApplications, withdrawalQuery]); + const loadCoinSellerRechargeOrders = useCallback(async () => { + if (!session?.canViewCoinSellerRechargeOrders) { + return; + } + setCoinSellerRechargeOrdersState((current) => ({ ...current, error: "", loading: true })); + try { + const data = await listFinanceCoinSellerRechargeOrders(coinSellerRechargeOrderQuery); + setCoinSellerRechargeOrdersState({ data, error: "", loading: false }); + } catch (err) { + setCoinSellerRechargeOrdersState((current) => ({ + ...current, + error: err.message || "加载币商充值订单失败", + loading: false, + })); + } + }, [coinSellerRechargeOrderQuery, session?.canViewCoinSellerRechargeOrders]); + useEffect(() => { loadOptions(); }, [loadOptions]); @@ -504,6 +557,10 @@ export function FinanceApp() { loadWithdrawals(); }, [loadWithdrawals]); + useEffect(() => { + loadCoinSellerRechargeOrders(); + }, [loadCoinSellerRechargeOrders]); + const operationOptions = useMemo( () => filterOperationsByPermissions(optionsState.data.operations, session?.permissions || []), [optionsState.data.operations, session?.permissions], @@ -580,6 +637,72 @@ export function FinanceApp() { } }; + const submitCoinSellerRechargeOrder = async (payload) => { + if (!session?.canCreateCoinSellerRechargeOrder) { + showToast("无创建币商充值订单权限", "error"); + return null; + } + setCoinSellerRechargeOrderActionLoading("create"); + try { + const order = await createFinanceCoinSellerRechargeOrder(cleanPayload(payload)); + showToast("币商充值订单已创建", "success"); + await loadCoinSellerRechargeOrders(); + return order; + } catch (err) { + showToast(err.message || "创建币商充值订单失败", "error"); + return null; + } finally { + setCoinSellerRechargeOrderActionLoading(""); + } + }; + + const verifyCoinSellerRechargeReceipt = async (payload) => { + if (!session?.canVerifyCoinSellerRechargeOrder) { + showToast("无校验币商充值订单权限", "error"); + return null; + } + try { + const verification = await verifyFinanceCoinSellerRechargeReceipt(cleanPayload(payload)); + showToast(verification?.verified ? "订单号校验通过" : verification?.failureReason || "订单号校验未通过", verification?.verified ? "success" : "error"); + return verification; + } catch (err) { + showToast(err.message || "校验订单号失败", "error"); + return null; + } + }; + + const verifyCoinSellerRechargeOrder = async (order) => { + if (!order?.id || !session?.canVerifyCoinSellerRechargeOrder) { + return; + } + setCoinSellerRechargeOrderActionLoading(`verify:${order.id}`); + try { + await verifyFinanceCoinSellerRechargeOrder(order.id); + showToast("订单校验已完成", "success"); + await loadCoinSellerRechargeOrders(); + } catch (err) { + showToast(err.message || "校验币商充值订单失败", "error"); + } finally { + setCoinSellerRechargeOrderActionLoading(""); + } + }; + + const grantCoinSellerRechargeOrder = async (order) => { + if (!order?.id || !session?.canGrantCoinSellerRechargeOrder) { + return; + } + setCoinSellerRechargeOrderActionLoading(`grant:${order.id}`); + try { + await grantFinanceCoinSellerRechargeOrder(order.id); + showToast("币商充值已发放", "success"); + await loadCoinSellerRechargeOrders(); + } catch (err) { + showToast(err.message || "发放币商充值失败", "error"); + } finally { + setCoinSellerRechargeOrderActionLoading(""); + } + }; + const updateFilters = (nextFilters) => { setFilters((current) => ({ ...current, ...nextFilters, page: nextFilters.page || 1 })); }; @@ -596,6 +719,10 @@ export function FinanceApp() { setWithdrawalFilters((current) => ({ ...current, ...nextFilters, page: nextFilters.page || 1 })); }; + const updateCoinSellerRechargeOrderFilters = (nextFilters) => { + setCoinSellerRechargeOrderFilters((current) => ({ ...current, ...nextFilters, page: nextFilters.page || 1 })); + }; + const closeCreateDialog = (_, reason) => { if (submitting || reason === "backdropClick") { return; @@ -695,6 +822,7 @@ export function FinanceApp() { appBar={appBar} canAudit={session.canAuditApplication} canCreate={session.canCreateApplication} + canViewCoinSellerRechargeOrders={session.canViewCoinSellerRechargeOrders} canViewRechargeDetails={session.canViewAppRechargeDetails} canViewWithdrawals={session.canViewWithdrawalApplications} loading={ @@ -702,7 +830,8 @@ export function FinanceApp() { rechargeDetailsState.loading || myApplicationsState.loading || applicationsState.loading || - withdrawalsState.loading + withdrawalsState.loading || + coinSellerRechargeOrdersState.loading } session={session} toolbar={financeToolbar} @@ -730,6 +859,24 @@ export function FinanceApp() { }} onOpenWithdrawals={() => setActiveView("withdrawals")} /> + ) : activeView === "coinSellerRechargeOrders" && session.canViewCoinSellerRechargeOrders ? ( + ) : activeView === "recon" && session.canViewAppRechargeDetails ? ( ({ approveFinanceApplication: vi.fn(), approveFinanceWithdrawalApplication: vi.fn(), createFinanceApplication: vi.fn(), + createFinanceCoinSellerRechargeOrder: vi.fn(), fetchFinanceApplicationOptions: vi.fn(async () => ({ apps: [{ appCode: "lalu", appName: "lalu" }], operations: [{ label: "增加用户金币", permissionCode: "finance-operation:user-coin-credit", value: "user_coin_credit" }], @@ -16,18 +17,25 @@ vi.mock("./api.js", () => ({ canAccessFinance: true, canAuditApplication: false, canCreateApplication: true, + canCreateCoinSellerRechargeOrder: false, + canGrantCoinSellerRechargeOrder: false, + canVerifyCoinSellerRechargeOrder: false, canViewAppRechargeDetails: false, + canViewCoinSellerRechargeOrders: false, canViewWithdrawalApplications: false, permissions: ["finance-application:create", "finance-operation:user-coin-credit"], user: { account: "admin", name: "admin" } })), filterOperationsByPermissions: vi.fn((operations, permissions) => operations.filter((item) => permissions.includes(item.permissionCode))), + grantFinanceCoinSellerRechargeOrder: vi.fn(), listFinanceApplications: vi.fn(), + listFinanceCoinSellerRechargeOrders: vi.fn(async () => ({ items: [], page: 1, pageSize: 50, total: 0 })), listFinanceRechargeDetails: vi.fn(), listFinanceWithdrawalApplications: vi.fn(), listMyFinanceApplications: vi.fn(async () => ({ items: [], page: 1, pageSize: 50, total: 0 })), rejectFinanceApplication: vi.fn(), - rejectFinanceWithdrawalApplication: vi.fn() + rejectFinanceWithdrawalApplication: vi.fn(), + verifyFinanceCoinSellerRechargeOrder: vi.fn() })); afterEach(() => { diff --git a/finance/src/api.js b/finance/src/api.js index 033559e..f699f46 100644 --- a/finance/src/api.js +++ b/finance/src/api.js @@ -6,6 +6,8 @@ import { mergeRechargeOverviews, mergeRechargeSummaries, normalizeApplicationPage, + normalizeCoinSellerRechargeOrder, + normalizeCoinSellerRechargeOrderPage, normalizeRechargeBillPage, normalizeRechargeOverview, normalizeRechargeRegions, @@ -24,14 +26,27 @@ export async function fetchFinanceSession() { permissions.includes(FINANCE_PERMISSIONS.auditWithdrawalApplications) || permissions.includes(FINANCE_PERMISSIONS.auditApplication); const canViewWithdrawalApplications = permissions.includes(FINANCE_PERMISSIONS.viewWithdrawalApplications) || canAuditWithdrawalApplications; const canViewAppRechargeDetails = permissions.includes(FINANCE_PERMISSIONS.viewAppRechargeDetails); + const canViewCoinSellerRechargeOrders = permissions.includes(FINANCE_PERMISSIONS.viewCoinSellerRechargeOrders); + const canCreateCoinSellerRechargeOrder = permissions.includes(FINANCE_PERMISSIONS.createCoinSellerRechargeOrder); + const canVerifyCoinSellerRechargeOrder = permissions.includes(FINANCE_PERMISSIONS.verifyCoinSellerRechargeOrder); + const canGrantCoinSellerRechargeOrder = permissions.includes(FINANCE_PERMISSIONS.grantCoinSellerRechargeOrder); return { ...session, - canAccessFinance: canCreateApplication || canAuditApplication || canViewWithdrawalApplications || canViewAppRechargeDetails, + canAccessFinance: + canCreateApplication || + canAuditApplication || + canViewWithdrawalApplications || + canViewAppRechargeDetails || + canViewCoinSellerRechargeOrders, canAuditApplication, canAuditWithdrawalApplications, canCreateApplication, + canCreateCoinSellerRechargeOrder, + canGrantCoinSellerRechargeOrder, + canVerifyCoinSellerRechargeOrder, canViewAppRechargeDetails, + canViewCoinSellerRechargeOrders, canViewWithdrawalApplications, permissions }; @@ -81,6 +96,48 @@ export async function listFinanceWithdrawalApplications(query = {}) { return normalizeWithdrawalApplicationPage(data); } +export async function listFinanceCoinSellerRechargeOrders(query = {}) { + const endpoint = API_ENDPOINTS.listFinanceCoinSellerRechargeOrders; + const data = await apiRequest(apiEndpointPath(API_OPERATIONS.listFinanceCoinSellerRechargeOrders), { + method: endpoint.method, + query + }); + return normalizeCoinSellerRechargeOrderPage(data); +} + +export async function createFinanceCoinSellerRechargeOrder(payload) { + const endpoint = API_ENDPOINTS.createFinanceCoinSellerRechargeOrder; + const data = await apiRequest(apiEndpointPath(API_OPERATIONS.createFinanceCoinSellerRechargeOrder), { + body: payload, + method: endpoint.method + }); + return normalizeCoinSellerRechargeOrder(data); +} + +export async function verifyFinanceCoinSellerRechargeReceipt(payload) { + const endpoint = API_ENDPOINTS.verifyFinanceCoinSellerRechargeReceipt; + return apiRequest(apiEndpointPath(API_OPERATIONS.verifyFinanceCoinSellerRechargeReceipt), { + body: cleanPayload(payload), + method: endpoint.method + }); +} + +export async function verifyFinanceCoinSellerRechargeOrder(orderId) { + const endpoint = API_ENDPOINTS.verifyFinanceCoinSellerRechargeOrder; + const data = await apiRequest(apiEndpointPath(API_OPERATIONS.verifyFinanceCoinSellerRechargeOrder, { order_id: orderId }), { + method: endpoint.method + }); + return normalizeCoinSellerRechargeOrder(data); +} + +export async function grantFinanceCoinSellerRechargeOrder(orderId) { + const endpoint = API_ENDPOINTS.grantFinanceCoinSellerRechargeOrder; + const data = await apiRequest(apiEndpointPath(API_OPERATIONS.grantFinanceCoinSellerRechargeOrder, { order_id: orderId }), { + method: endpoint.method + }); + return normalizeCoinSellerRechargeOrder(data); +} + export async function listFinanceRechargeDetails(query = {}, apps = []) { const page = positiveInteger(query.page, 1); const pageSize = positiveInteger(query.page_size ?? query.pageSize, 50); @@ -326,6 +383,12 @@ function cleanRechargeBillQuery(query = {}) { return rest; } +function cleanPayload(payload = {}) { + return Object.fromEntries( + Object.entries(payload).filter(([, value]) => value !== undefined && value !== null && value !== "") + ); +} + function positiveInteger(value, fallback) { const number = Number(value); return Number.isInteger(number) && number > 0 ? number : fallback; diff --git a/finance/src/api.test.js b/finance/src/api.test.js index e845d36..8175a5c 100644 --- a/finance/src/api.test.js +++ b/finance/src/api.test.js @@ -4,14 +4,19 @@ import { approveFinanceApplication, approveFinanceWithdrawalApplication, createFinanceApplication, + createFinanceCoinSellerRechargeOrder, fetchFinanceApplicationOptions, filterOperationsByPermissions, + grantFinanceCoinSellerRechargeOrder, listFinanceApplications, + listFinanceCoinSellerRechargeOrders, listFinanceRechargeDetails, listMyFinanceApplications, listFinanceWithdrawalApplications, rejectFinanceApplication, - rejectFinanceWithdrawalApplication + rejectFinanceWithdrawalApplication, + verifyFinanceCoinSellerRechargeReceipt, + verifyFinanceCoinSellerRechargeOrder } from "./api.js"; import { FINANCE_OPERATIONS } from "./constants.js"; @@ -92,6 +97,56 @@ test("finance application APIs use generated endpoint paths", async () => { expect(JSON.parse(String(calls[8][1]?.body))).toEqual({ auditRemark: "bad" }); }); +test("finance coin seller recharge order APIs use generated endpoint paths", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async (url) => { + if (String(url).includes("/coin-seller-recharges?")) { + return new Response(JSON.stringify({ code: 0, data: { items: [], page: 1, pageSize: 50, total: 0 } })); + } + return new Response(JSON.stringify({ code: 0, data: { id: "order-1", targetUserId: "10001" } })); + }) + ); + + await listFinanceCoinSellerRechargeOrders({ page: 1, page_size: 50, provider_code: "mifapay" }); + await createFinanceCoinSellerRechargeOrder({ + appCode: "lalu", + coinAmount: 100000, + externalOrderNo: "MIFA-001", + providerCode: "mifapay", + targetUserId: "10001", + usdAmount: 10 + }); + await verifyFinanceCoinSellerRechargeReceipt({ + appCode: "lalu", + externalOrderNo: "MIFA-001", + providerCode: "mifapay", + usdAmount: 10 + }); + await verifyFinanceCoinSellerRechargeOrder("order-1"); + await grantFinanceCoinSellerRechargeOrder("order-1"); + + const calls = vi.mocked(fetch).mock.calls; + expect(String(calls[0][0])).toContain("/api/v1/admin/finance/orders/coin-seller-recharges?"); + expect(String(calls[0][0])).toContain("provider_code=mifapay"); + expect(calls[0][1]?.method).toBe("GET"); + expect(String(calls[1][0])).toContain("/api/v1/admin/finance/orders/coin-seller-recharges"); + expect(calls[1][1]?.method).toBe("POST"); + expect(JSON.parse(String(calls[1][1]?.body))).toMatchObject({ externalOrderNo: "MIFA-001", providerCode: "mifapay" }); + expect(String(calls[2][0])).toContain("/api/v1/admin/finance/orders/coin-seller-recharges/receipt-verifications"); + expect(calls[2][1]?.method).toBe("POST"); + expect(JSON.parse(String(calls[2][1]?.body))).toEqual({ + appCode: "lalu", + externalOrderNo: "MIFA-001", + providerCode: "mifapay", + usdAmount: 10 + }); + expect(String(calls[3][0])).toContain("/api/v1/admin/finance/orders/coin-seller-recharges/order-1/verify"); + expect(calls[3][1]?.method).toBe("POST"); + expect(String(calls[4][0])).toContain("/api/v1/admin/finance/orders/coin-seller-recharges/order-1/grant"); + expect(calls[4][1]?.method).toBe("POST"); +}); + test("filters operation options by backend-configured permissions", () => { const allowed = filterOperationsByPermissions(FINANCE_OPERATIONS, [ "finance-operation:user-coin-credit", diff --git a/finance/src/components/FinanceCoinSellerRechargeOrderList.jsx b/finance/src/components/FinanceCoinSellerRechargeOrderList.jsx new file mode 100644 index 0000000..d54f93e --- /dev/null +++ b/finance/src/components/FinanceCoinSellerRechargeOrderList.jsx @@ -0,0 +1,539 @@ +import AddOutlined from "@mui/icons-material/AddOutlined"; +import FactCheckOutlined from "@mui/icons-material/FactCheckOutlined"; +import PaidOutlined from "@mui/icons-material/PaidOutlined"; +import RefreshOutlined from "@mui/icons-material/RefreshOutlined"; +import Alert from "@mui/material/Alert"; +import Button from "@mui/material/Button"; +import Dialog from "@mui/material/Dialog"; +import DialogActions from "@mui/material/DialogActions"; +import DialogContent from "@mui/material/DialogContent"; +import DialogTitle from "@mui/material/DialogTitle"; +import InputAdornment from "@mui/material/InputAdornment"; +import MenuItem from "@mui/material/MenuItem"; +import TextField from "@mui/material/TextField"; +import { useState } from "react"; +import { + COIN_SELLER_RECHARGE_CHAINS, + COIN_SELLER_RECHARGE_PROVIDERS, + COIN_SELLER_RECHARGE_STATUS, + DEFAULT_COIN_SELLER_RECHARGE_ORDER_FORM, +} from "../constants.js"; +import { + buildCoinSellerRechargeReceiptPayload, + buildCoinSellerRechargeOrderPayload, + coinSellerRechargeChainLabel, + coinSellerRechargeGrantStatusLabel, + coinSellerRechargeProviderLabel, + coinSellerRechargeStatusLabel, + coinSellerRechargeStatusTone, + coinSellerRechargeVerifyStatusLabel, + formatAmount, + formatTime, + formatUsdMinor, + isCoinSellerRechargeGranted, + isCoinSellerRechargeVerified, + validateCoinSellerRechargeOrderPayload, +} from "../format.js"; + +export function FinanceCoinSellerRechargeOrderList({ + actionLoading, + apps, + canCreate, + canGrant, + canVerify, + error, + filters, + loading, + onCreate, + onFiltersChange, + onGrant, + onReload, + onVerify, + onVerifyReceipt, + orders, +}) { + const [createOpen, setCreateOpen] = useState(false); + const [form, setForm] = useState(DEFAULT_COIN_SELLER_RECHARGE_ORDER_FORM); + const [formError, setFormError] = useState(""); + const [receiptVerification, setReceiptVerification] = useState(null); + const [receiptLoading, setReceiptLoading] = useState(false); + const items = orders.items || []; + const page = Number(orders.page || filters.page || 1); + const pageSize = Number(orders.pageSize || 50); + const total = Number(orders.total || 0); + const hasNextPage = page * pageSize < total; + + const openCreate = () => { + setForm({ + ...DEFAULT_COIN_SELLER_RECHARGE_ORDER_FORM, + appCode: filters.appCode || apps[0]?.appCode || "", + }); + setFormError(""); + setReceiptVerification(null); + setCreateOpen(true); + }; + + const updateForm = (key, value) => { + if (key === "providerCode") { + setForm((current) => ({ + ...current, + chain: "", + providerCode: value, + })); + setFormError(""); + setReceiptVerification(null); + return; + } + setForm((current) => ({ ...current, [key]: value })); + setFormError(""); + // 订单号校验结果绑定当前 APP、用户输入金额、渠道、链和订单号;任一字段变化都不能复用旧凭证。 + setReceiptVerification(null); + }; + + const submitCreate = async (event) => { + event.preventDefault(); + if (!receiptVerification?.verified) { + setFormError("请先校验订单号"); + return; + } + const payload = buildCoinSellerRechargeOrderPayload(form); + const validationMessage = validateCoinSellerRechargeOrderPayload(payload); + if (validationMessage) { + setFormError(validationMessage); + return; + } + const created = await onCreate(payload); + if (created) { + setCreateOpen(false); + setReceiptVerification(null); + } + }; + + const verifyCreateReceipt = async () => { + const payload = buildCoinSellerRechargeReceiptPayload(form); + const validationMessage = validateCoinSellerRechargeOrderPayload({ + ...buildCoinSellerRechargeOrderPayload(form), + coinAmount: 1, + targetUserId: "verification-only", + }); + if (validationMessage) { + setFormError(validationMessage); + return; + } + setFormError(""); + setReceiptLoading(true); + try { + const verification = await onVerifyReceipt(payload); + setReceiptVerification(verification); + if (!verification?.verified) { + setFormError(verification?.failureReason || "订单号校验未通过"); + } + } finally { + setReceiptLoading(false); + } + }; + + const confirmGrant = (order) => { + const userText = order.targetDisplayUserId || order.targetUserId || "-"; + const ok = window.confirm( + isZeroCoinMakeupOrder(order) + ? `确认完成用户 ${userText} 的补单?该订单计入充值,不发放金币。` + : `确认给用户 ${userText} 发放 ${formatAmount(order.coinAmount)} 金币?`, + ); + if (ok) { + onGrant(order); + } + }; + + return ( +
+
+ onFiltersChange({ appCode: event.target.value })} + > + 全部 APP + {apps.map((app) => ( + + {app.appName || app.appCode} + + ))} + + onFiltersChange({ providerCode: event.target.value })} + > + 全部渠道 + {COIN_SELLER_RECHARGE_PROVIDERS.map(([value, label]) => ( + + {label} + + ))} + + onFiltersChange({ status: event.target.value })} + > + 全部状态 + {COIN_SELLER_RECHARGE_STATUS.map(([value, label]) => ( + + {label} + + ))} + + onFiltersChange({ keyword: event.target.value })} + /> + + + +
+ + {error ?
{error}
: null} + {loading && !items.length ?
: null} + {!loading || items.length ? ( +
+ + + + + + + + + + + + + + + + + + + {items.length ? ( + items.map((item) => ( + + + + + + + + + + + + + + + )) + ) : ( + + + + )} + +
订单APP目标用户金额渠道三方订单备注状态钱包操作人时间操作
{item.id || "-"}{appName(item.appCode, apps)} + + + + + + + + {item.remark || "-"} + } + secondary={`${coinSellerRechargeVerifyStatusLabel(item.verifyStatus)} / ${coinSellerRechargeGrantStatusLabel(item.grantStatus)}`} + /> + + + + + + + + + + + +
+ 当前无数据 +
+
+ ) : null} + {total > pageSize ? ( +
+ + + 第 {page} 页 / 共 {total} 条 + + +
+ ) : null} + { + setCreateOpen(false); + setReceiptVerification(null); + setFormError(""); + }} + onSubmit={submitCreate} + onVerifyReceipt={verifyCreateReceipt} + /> +
+ ); +} + +function CreateDialog({ + apps, + form, + formError, + loading, + onChange, + onClose, + onSubmit, + onVerifyReceipt, + open, + receiptLoading, + receiptVerification, +}) { + const disabled = loading || receiptLoading; + const canCreate = receiptVerification?.verified === true && !disabled; + + return ( + + 创建币商充值 + +
+ {formError ? {formError} : null} +
+ {apps.length ? ( + onChange("appCode", event.target.value)}> + {apps.map((app) => ( + + {app.appName || app.appCode} + + ))} + + ) : ( + onChange("appCode", event.target.value)} /> + )} + onChange("targetUserId", event.target.value)} /> + USD }} + inputProps={{ min: 0, step: "0.01" }} + label="USD 金额" + required + type="number" + value={form.usdAmount} + onChange={(event) => onChange("usdAmount", event.target.value)} + /> + 金币 }} + inputProps={{ min: 0, step: "1" }} + label="充值金币" + required + type="number" + value={form.coinAmount} + onChange={(event) => onChange("coinAmount", event.target.value)} + /> + onChange("providerCode", event.target.value)}> + {COIN_SELLER_RECHARGE_PROVIDERS.map(([value, label]) => ( + + {label} + + ))} + + {form.providerCode === "usdt" ? ( + onChange("chain", event.target.value)}> + {COIN_SELLER_RECHARGE_CHAINS.map(([value, label]) => ( + + {label} + + ))} + + ) : null} +
+ onChange("externalOrderNo", event.target.value)} /> + +
+ onChange("remark", event.target.value)} + /> +
+ {receiptVerification ? : null} + +
+ + + + +
+ ); +} + +function isZeroCoinMakeupOrder(order) { + return Number(order?.coinAmount || 0) === 0; +} + +function ReceiptVerificationBanner({ verification }) { + const detail = [ + verification.status && `状态 ${verification.status}`, + verification.providerOrderId && `三方单 ${verification.providerOrderId}`, + verification.currencyCode && verification.providerAmountMinor !== undefined + ? formatUsdMinor(verification.providerAmountMinor, verification.currencyCode) + : "", + verification.failureReason, + ] + .filter(Boolean) + .join(" / "); + return ( + + {verification.verified ? "校验通过" : "校验未通过"} + {detail ? `:${detail}` : ""} + + ); +} + +function StatusBadge({ status }) { + return {coinSellerRechargeStatusLabel(status)}; +} + +function Stacked({ primary, secondary }) { + return ( + + {primary || "-"} + {secondary || ""} + + ); +} + +function appName(appCode, apps) { + return apps.find((app) => app.appCode === appCode)?.appName || appCode || "-"; +} + +function usdText(item) { + if (item.usdAmount !== undefined && item.usdAmount !== null && item.usdAmount !== "") { + return `USD ${item.usdAmount}`; + } + return formatUsdMinor(item.usdMinorAmount, "USD"); +} + +function providerAmountText(item) { + return item.providerAmountMinor === null || item.providerAmountMinor === undefined + ? "-" + : formatUsdMinor(item.providerAmountMinor, item.providerCurrencyCode || "USD"); +} + +function providerMeta(item) { + if (item.providerCode === "usdt") { + return [coinSellerRechargeChainLabel(item.chain), item.providerStatus].filter(Boolean).join(" / ") || "-"; + } + if (item.providerCode === "mifapay") { + return [item.providerStatus, item.providerOrderId].filter(Boolean).join(" / ") || "-"; + } + if (item.providerCode === "v5pay") { + return [item.providerStatus, item.providerCurrencyCode, item.providerOrderId].filter(Boolean).join(" / ") || "-"; + } + return "-"; +} + +function walletMeta(item) { + return [ + item.walletAssetType, + item.walletAmountDelta === null || item.walletAmountDelta === undefined ? "" : `变动 ${formatAmount(item.walletAmountDelta)}`, + item.walletBalanceAfter === null || item.walletBalanceAfter === undefined ? "" : `余额 ${formatAmount(item.walletBalanceAfter)}`, + ] + .filter(Boolean) + .join(" / "); +} diff --git a/finance/src/components/FinanceCoinSellerRechargeOrderList.test.jsx b/finance/src/components/FinanceCoinSellerRechargeOrderList.test.jsx new file mode 100644 index 0000000..9af7750 --- /dev/null +++ b/finance/src/components/FinanceCoinSellerRechargeOrderList.test.jsx @@ -0,0 +1,170 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { afterEach, expect, test, vi } from "vitest"; +import { FinanceCoinSellerRechargeOrderList } from "./FinanceCoinSellerRechargeOrderList.jsx"; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +test("coin seller recharge order create dialog verifies receipt before create", async () => { + const onCreate = vi.fn(async () => ({ id: "order-2" })); + const onVerifyReceipt = vi.fn(async () => ({ + currencyCode: "USD", + providerAmountMinor: 1000, + providerOrderId: "V5-001", + status: "paid", + verified: true, + })); + render(); + + fireEvent.click(screen.getByRole("button", { name: "创建币商充值" })); + fireEvent.mouseDown(screen.getByRole("combobox", { name: "支付渠道" })); + fireEvent.click(screen.getByRole("option", { name: "V5Pay" })); + + expect(screen.queryByRole("textbox", { name: "三方国家码" })).not.toBeInTheDocument(); + expect(screen.queryByRole("textbox", { name: "三方币种" })).not.toBeInTheDocument(); + expect(screen.queryByRole("textbox", { name: "支付类型" })).not.toBeInTheDocument(); + expect(screen.queryByRole("textbox", { name: "订单日期" })).not.toBeInTheDocument(); + + fireEvent.change(screen.getByRole("textbox", { name: "目标用户 ID" }), { target: { value: "10001" } }); + fireEvent.change(screen.getByRole("spinbutton", { name: "USD 金额" }), { target: { value: "10" } }); + fireEvent.change(screen.getByRole("spinbutton", { name: "充值金币" }), { target: { value: "0" } }); + fireEvent.change(screen.getByRole("textbox", { name: "三方订单号" }), { target: { value: "V5-001" } }); + fireEvent.change(screen.getByRole("textbox", { name: "备注" }), { target: { value: "补单,不发金币" } }); + expect(screen.getByRole("button", { name: "创建" })).toBeDisabled(); + + fireEvent.click(screen.getByRole("button", { name: "校验" })); + await screen.findByText(/校验通过/); + await waitFor(() => expect(screen.getByRole("button", { name: "创建" })).not.toBeDisabled()); + expect(onVerifyReceipt).toHaveBeenCalledWith({ + appCode: "lalu", + externalOrderNo: "V5-001", + providerCode: "v5pay", + usdAmount: 10, + }); + fireEvent.click(screen.getByRole("button", { name: "创建" })); + await waitFor(() => + expect(onCreate).toHaveBeenCalledWith({ + appCode: "lalu", + coinAmount: 0, + externalOrderNo: "V5-001", + providerCode: "v5pay", + remark: "补单,不发金币", + targetUserId: "10001", + usdAmount: 10, + }), + ); +}); + +test("coin seller recharge order actions follow verify and grant status", () => { + const onVerify = vi.fn(); + const onGrant = vi.fn(); + vi.stubGlobal("confirm", vi.fn(() => true)); + + render( + , + ); + + const verifyButtons = screen.getAllByRole("button", { name: "校验" }); + const grantButtons = screen.getAllByRole("button", { name: "发放" }); + fireEvent.click(verifyButtons[0]); + fireEvent.click(grantButtons[1]); + + expect(onVerify).toHaveBeenCalledWith(expect.objectContaining({ id: "pending" })); + expect(onGrant).toHaveBeenCalledWith(expect.objectContaining({ id: "verified" })); + expect(screen.getAllByText("人工补单")[0]).toBeInTheDocument(); + expect(grantButtons[0]).toBeDisabled(); + expect(verifyButtons[1]).toBeDisabled(); +}); + +test("zero coin seller recharge order uses makeup completion action", () => { + const onGrant = vi.fn(); + const confirm = vi.fn(() => true); + vi.stubGlobal("confirm", confirm); + + render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: "完成补单" })); + + expect(confirm).toHaveBeenCalledWith(expect.stringContaining("不发放金币")); + expect(onGrant).toHaveBeenCalledWith(expect.objectContaining({ id: "makeup", coinAmount: 0 })); +}); + +function baseProps(overrides = {}) { + return { + actionLoading: "", + apps: [{ appCode: "lalu", appName: "lalu" }], + canCreate: true, + canGrant: true, + canVerify: true, + error: "", + filters: { + appCode: "", + grantStatus: "", + keyword: "", + page: 1, + providerCode: "", + status: "", + verifyStatus: "", + }, + loading: false, + onCreate: vi.fn(), + onFiltersChange: vi.fn(), + onGrant: vi.fn(), + onReload: vi.fn(), + onVerify: vi.fn(), + onVerifyReceipt: vi.fn(), + orders: { + items: [orderFixture()], + page: 1, + pageSize: 50, + total: 1, + }, + ...overrides, + }; +} + +function orderFixture(overrides = {}) { + return { + appCode: "lalu", + coinAmount: 100000, + createdAtMs: 1760000000000, + externalOrderNo: "MIFA-001", + grantStatus: "pending", + id: "order-1", + providerCode: "mifapay", + remark: "人工补单", + status: "pending", + targetDisplayUserId: "165075", + targetUserId: "10001", + usdAmount: "10.00", + verifyStatus: "pending", + ...overrides, + }; +} diff --git a/finance/src/components/FinanceRechargeDetailList.test.jsx b/finance/src/components/FinanceRechargeDetailList.test.jsx index 15393a3..eaeba7d 100644 --- a/finance/src/components/FinanceRechargeDetailList.test.jsx +++ b/finance/src/components/FinanceRechargeDetailList.test.jsx @@ -1,8 +1,12 @@ -import { fireEvent, render, screen } from "@testing-library/react"; +import { render, screen } from "@testing-library/react"; import { expect, test, vi } from "vitest"; import { FinanceRechargeDetailList } from "./FinanceRechargeDetailList.jsx"; const baseProps = { + apps: [ + { appCode: "lalu", appName: "lalu" }, + { appCode: "aslan", appName: "Aslan" }, + ], bills: { items: [], page: 1, @@ -20,12 +24,6 @@ const baseProps = { loading: false, onFiltersChange: vi.fn(), onReload: vi.fn(), - options: { - apps: [ - { appCode: "lalu", appName: "lalu" }, - { appCode: "aslan", appName: "Aslan" }, - ], - }, summary: { googlePlay: { billCount: 1, coinAmount: 90000, usdMinorAmount: 999 }, thirdParty: { billCount: 0, coinAmount: 0, usdMinorAmount: 0 }, @@ -46,18 +44,14 @@ test("hides coin recharge columns by default", () => { />, ); - expect(screen.getByRole("switch", { name: "金币显示" })).not.toBeChecked(); expect(screen.queryByRole("columnheader", { name: "充值金币" })).not.toBeInTheDocument(); - expect(screen.queryByRole("columnheader", { name: "金币换算" })).not.toBeInTheDocument(); expect(screen.queryByText("90,000 金币")).not.toBeInTheDocument(); - expect(screen.queryByText("90,000 金币 = USD 9.99")).not.toBeInTheDocument(); expect(screen.queryByText("1 笔 · 90,000 金币")).not.toBeInTheDocument(); - expect(screen.getAllByText("1 笔")).toHaveLength(2); expect(screen.getByRole("columnheader", { name: "账单金额" })).toBeInTheDocument(); expect(screen.getByRole("columnheader", { name: "用户实付" })).toBeInTheDocument(); }); -test("renders app recharge coin columns and values when switch is enabled", () => { +test("renders app recharge coin columns and values when coin currency is selected", () => { render( , ); - fireEvent.click(screen.getByRole("switch", { name: "金币显示" })); - expect(screen.getByRole("columnheader", { name: "APP" })).toBeInTheDocument(); expect(screen.getByRole("columnheader", { name: "充值金币" })).toBeInTheDocument(); expect(screen.getByRole("columnheader", { name: "充值来源" })).toBeInTheDocument(); expect(screen.getByRole("columnheader", { name: "充值订单号" })).toBeInTheDocument(); - expect(screen.getByRole("columnheader", { name: "金币换算" })).toBeInTheDocument(); expect(screen.getByRole("columnheader", { name: "账单金额" })).toBeInTheDocument(); expect(screen.getByRole("columnheader", { name: "用户实付" })).toBeInTheDocument(); expect(screen.getByRole("columnheader", { name: "三方税率扣款" })).toBeInTheDocument(); expect(screen.getByText("Aslan")).toBeInTheDocument(); expect(screen.getByText("90,000 金币")).toBeInTheDocument(); - expect(screen.getAllByText("1 笔 · 90,000 金币")).toHaveLength(2); expect(screen.getAllByText("谷歌充值").length).toBeGreaterThanOrEqual(1); expect(screen.getByText("tx-google")).toBeInTheDocument(); expect(screen.getByText("cmd-1 / GPA.1")).toBeInTheDocument(); - expect(screen.getByText("90,000 金币 = USD 9.99")).toBeInTheDocument(); expect(screen.getAllByText("SAR 12.99")).toHaveLength(2); expect(screen.getByText("SAR 1.95 / 15%")).toBeInTheDocument(); - expect(screen.getByRole("button", { name: /充值时间 · 中国时区/ })).toBeInTheDocument(); }); test("keeps empty recharge detail table colspan aligned with visible columns", () => { const { container } = render(); - expect(container.querySelector("tbody td")?.colSpan).toBe(7); - expect(screen.getByText("当前无数据")).toBeInTheDocument(); + expect(container.querySelector("tbody td")?.colSpan).toBe(8); + expect(screen.getByText("当前筛选条件下没有账单")).toBeInTheDocument(); - fireEvent.click(screen.getByRole("switch", { name: "金币显示" })); - expect(container.querySelector("tbody td")?.colSpan).toBe(9); + const { container: coinContainer } = render(); + expect(coinContainer.querySelector("tbody td")?.colSpan).toBe(9); }); function rechargeBillFixture() { diff --git a/finance/src/components/FinanceShell.jsx b/finance/src/components/FinanceShell.jsx index f8a1ba2..9c395ab 100644 --- a/finance/src/components/FinanceShell.jsx +++ b/finance/src/components/FinanceShell.jsx @@ -7,6 +7,10 @@ import RuleFolderOutlined from "@mui/icons-material/RuleFolderOutlined"; import LinearProgress from "@mui/material/LinearProgress"; const NAV_GROUPS = [ + { + label: "订单", + views: [{ icon: ReceiptLongOutlined, id: "coinSellerRechargeOrders", label: "币商充值" }], + }, { label: "经营", views: [ @@ -27,6 +31,7 @@ const NAV_GROUPS = [ const VIEW_TITLES = { applications: "申请审核", + coinSellerRechargeOrders: "币商充值", create: "发起申请", overview: "财务概览", recon: "渠道对账", @@ -39,6 +44,7 @@ export function FinanceShell({ appBar, canAudit, canCreate, + canViewCoinSellerRechargeOrders, canViewRechargeDetails, canViewWithdrawals, children, @@ -52,6 +58,9 @@ export function FinanceShell({ if (viewId === "overview" || viewId === "rechargeDetails" || viewId === "recon") { return canViewRechargeDetails; } + if (viewId === "coinSellerRechargeOrders") { + return canViewCoinSellerRechargeOrders; + } if (viewId === "applications") { return canAudit; } diff --git a/finance/src/constants.js b/finance/src/constants.js index 3db4e10..81cfd48 100644 --- a/finance/src/constants.js +++ b/finance/src/constants.js @@ -2,6 +2,10 @@ export const FINANCE_PERMISSIONS = { auditApplication: "finance-application:audit", auditWithdrawalApplications: "finance-withdrawal:audit", createApplication: "finance-application:create", + createCoinSellerRechargeOrder: "finance-order:coin-seller-recharge:create", + grantCoinSellerRechargeOrder: "finance-order:coin-seller-recharge:grant", + verifyCoinSellerRechargeOrder: "finance-order:coin-seller-recharge:verify", + viewCoinSellerRechargeOrders: "finance-order:coin-seller-recharge:view", viewAppRechargeDetails: "payment-bill:view", viewWithdrawalApplications: "finance-withdrawal:view", }; @@ -55,6 +59,37 @@ export const APPLICATION_STATUS = [ ["rejected", "已拒绝"], ]; +export const COIN_SELLER_RECHARGE_PROVIDERS = [ + ["mifapay", "MiFaPay"], + ["v5pay", "V5Pay"], + ["usdt", "USDT"], +]; + +export const COIN_SELLER_RECHARGE_CHAINS = [ + ["TRON", "TRON"], + ["BSC", "BSC"], +]; + +export const COIN_SELLER_RECHARGE_STATUS = [ + ["pending", "待处理"], + ["verified", "已校验"], + ["granted", "已发放"], + ["failed", "失败"], + ["canceled", "已取消"], +]; + +export const COIN_SELLER_RECHARGE_VERIFY_STATUS = [ + ["pending", "待校验"], + ["verified", "校验通过"], + ["failed", "校验失败"], +]; + +export const COIN_SELLER_RECHARGE_GRANT_STATUS = [ + ["pending", "待发放"], + ["granted", "已发放"], + ["failed", "发放失败"], +]; + export const DEFAULT_APPLICATION_FORM = { appCode: "", coinAmount: "", @@ -66,6 +101,17 @@ export const DEFAULT_APPLICATION_FORM = { walletIdentity: "", }; +export const DEFAULT_COIN_SELLER_RECHARGE_ORDER_FORM = { + appCode: "", + chain: "", + coinAmount: "", + externalOrderNo: "", + providerCode: "mifapay", + remark: "", + targetUserId: "", + usdAmount: "", +}; + export function operationRequiresWalletIdentity(operation) { return FINANCE_OPERATIONS.some((item) => item.value === operation && item.requiresWalletIdentity); } diff --git a/finance/src/format.js b/finance/src/format.js index da7a171..8aabcb7 100644 --- a/finance/src/format.js +++ b/finance/src/format.js @@ -1,5 +1,10 @@ import { APPLICATION_STATUS, + COIN_SELLER_RECHARGE_CHAINS, + COIN_SELLER_RECHARGE_GRANT_STATUS, + COIN_SELLER_RECHARGE_PROVIDERS, + COIN_SELLER_RECHARGE_STATUS, + COIN_SELLER_RECHARGE_VERIFY_STATUS, FINANCE_OPERATIONS, WALLET_IDENTITIES, operationAllowsZeroRechargeAmount, @@ -122,6 +127,110 @@ export function normalizeWithdrawalApplicationPage(data = {}) { }; } +export function buildCoinSellerRechargeOrderPayload(form) { + const providerCode = stringValue(form.providerCode); + const payload = { + appCode: stringValue(form.appCode), + chain: providerCode === "usdt" ? stringValue(form.chain).toUpperCase() : "", + coinAmount: numberValue(form.coinAmount), + externalOrderNo: stringValue(form.externalOrderNo), + providerCode, + remark: stringValue(form.remark), + targetUserId: stringValue(form.targetUserId), + usdAmount: numberValue(form.usdAmount), + }; + + return Object.fromEntries(Object.entries(payload).filter(([, value]) => value !== "")); +} + +export function buildCoinSellerRechargeReceiptPayload(form) { + const providerCode = stringValue(form.providerCode); + return Object.fromEntries( + Object.entries({ + appCode: stringValue(form.appCode), + chain: providerCode === "usdt" ? stringValue(form.chain).toUpperCase() : "", + externalOrderNo: stringValue(form.externalOrderNo), + providerCode, + usdAmount: numberValue(form.usdAmount), + }).filter(([, value]) => value !== ""), + ); +} + +export function validateCoinSellerRechargeOrderPayload(payload) { + if (!payload.appCode) { + return "请选择 APP"; + } + if (!payload.targetUserId) { + return "请填写目标用户 ID"; + } + if (!Number.isFinite(payload.usdAmount) || payload.usdAmount <= 0) { + return "请填写大于 0 的 USD 金额"; + } + if (!Number.isInteger(payload.coinAmount) || payload.coinAmount < 0) { + return "请填写不小于 0 的整数金币"; + } + if (!payload.providerCode) { + return "请选择支付渠道"; + } + if (!payload.externalOrderNo) { + return "请填写三方订单号"; + } + if (payload.providerCode === "usdt" && !payload.chain) { + return "请选择 USDT 链"; + } + if (payload.remark && payload.remark.length > 512) { + return "备注不能超过 512 字"; + } + return ""; +} + +export function normalizeCoinSellerRechargeOrder(item = {}) { + return { + appCode: stringValue(item.appCode ?? item.app_code), + chain: stringValue(item.chain), + coinAmount: numberValue(item.coinAmount ?? item.coin_amount), + createdAtMs: numberOrNull(item.createdAtMs ?? item.created_at_ms), + externalOrderNo: stringValue(item.externalOrderNo ?? item.external_order_no), + failureReason: stringValue(item.failureReason ?? item.failure_reason), + grantStatus: stringValue(item.grantStatus ?? item.grant_status) || "pending", + grantedAtMs: numberOrNull(item.grantedAtMs ?? item.granted_at_ms), + grantedByName: stringValue(item.grantedByName ?? item.granted_by_name), + id: stringValue(item.id ?? item.orderId ?? item.order_id), + operatorName: stringValue(item.operatorName ?? item.operator_name), + providerAmountMinor: numberOrNull(item.providerAmountMinor ?? item.provider_amount_minor), + providerCode: stringValue(item.providerCode ?? item.provider_code), + providerCurrencyCode: stringValue(item.providerCurrencyCode ?? item.provider_currency_code), + providerOrderId: stringValue(item.providerOrderId ?? item.provider_order_id), + providerPayloadJson: stringValue(item.providerPayloadJson ?? item.provider_payload_json), + providerStatus: stringValue(item.providerStatus ?? item.provider_status), + receiveAddress: stringValue(item.receiveAddress ?? item.receive_address), + remark: stringValue(item.remark), + status: stringValue(item.status) || "pending", + targetDisplayUserId: stringValue(item.targetDisplayUserId ?? item.target_display_user_id), + targetUserId: stringValue(item.targetUserId ?? item.target_user_id), + usdAmount: item.usdAmount ?? item.usd_amount ?? "", + usdMinorAmount: numberOrNull(item.usdMinorAmount ?? item.usd_minor_amount), + verifiedAtMs: numberOrNull(item.verifiedAtMs ?? item.verified_at_ms), + verifiedByName: stringValue(item.verifiedByName ?? item.verified_by_name), + verifyStatus: stringValue(item.verifyStatus ?? item.verify_status) || "pending", + walletAmountDelta: numberOrNull(item.walletAmountDelta ?? item.wallet_amount_delta), + walletAssetType: stringValue(item.walletAssetType ?? item.wallet_asset_type), + walletBalanceAfter: numberOrNull(item.walletBalanceAfter ?? item.wallet_balance_after), + walletCommandId: stringValue(item.walletCommandId ?? item.wallet_command_id), + walletTransactionId: stringValue(item.walletTransactionId ?? item.wallet_transaction_id), + }; +} + +export function normalizeCoinSellerRechargeOrderPage(data = {}) { + const items = Array.isArray(data.items) ? data.items : []; + return { + items: items.map(normalizeCoinSellerRechargeOrder), + page: Number(data.page || 1), + pageSize: Number(data.pageSize ?? data.page_size ?? items.length), + total: Number(data.total ?? items.length), + }; +} + export function normalizeRechargeBill(item = {}) { return { appCode: stringValue(item.appCode ?? item.app_code), @@ -414,6 +523,50 @@ export function statusLabel(value) { return APPLICATION_STATUS.find(([status]) => status === value)?.[1] || value || "-"; } +export function coinSellerRechargeProviderLabel(value) { + return COIN_SELLER_RECHARGE_PROVIDERS.find(([code]) => code === value)?.[1] || value || "-"; +} + +export function coinSellerRechargeChainLabel(value) { + return COIN_SELLER_RECHARGE_CHAINS.find(([code]) => code === value)?.[1] || value || "-"; +} + +export function coinSellerRechargeStatusLabel(value) { + return COIN_SELLER_RECHARGE_STATUS.find(([status]) => status === value)?.[1] || value || "-"; +} + +export function coinSellerRechargeVerifyStatusLabel(value) { + return COIN_SELLER_RECHARGE_VERIFY_STATUS.find(([status]) => status === value)?.[1] || value || "-"; +} + +export function coinSellerRechargeGrantStatusLabel(value) { + return COIN_SELLER_RECHARGE_GRANT_STATUS.find(([status]) => status === value)?.[1] || value || "-"; +} + +export function coinSellerRechargeStatusTone(value) { + if (["granted", "verified", "success", "succeeded", "completed", "passed"].includes(stringValue(value))) { + return "success"; + } + if (["failed", "rejected", "canceled", "cancelled"].includes(stringValue(value))) { + return "danger"; + } + return "warning"; +} + +export function isCoinSellerRechargeVerified(order) { + return ( + ["verified", "success", "succeeded", "completed", "passed"].includes(stringValue(order?.verifyStatus)) || + stringValue(order?.status) === "verified" + ); +} + +export function isCoinSellerRechargeGranted(order) { + return ( + ["granted", "success", "succeeded", "completed"].includes(stringValue(order?.grantStatus)) || + stringValue(order?.status) === "granted" + ); +} + export function walletIdentityLabel(value) { return WALLET_IDENTITIES.find(([identity]) => identity === value)?.[1] || value || "-"; } diff --git a/finance/src/format.test.js b/finance/src/format.test.js index 56e9b34..89b0224 100644 --- a/finance/src/format.test.js +++ b/finance/src/format.test.js @@ -1,13 +1,18 @@ import { expect, test } from "vitest"; import { buildApplicationPayload, + buildCoinSellerRechargeReceiptPayload, + buildCoinSellerRechargeOrderPayload, + coinSellerRechargeProviderLabel, normalizeApplicationPage, + normalizeCoinSellerRechargeOrderPage, normalizeRechargeBillPage, normalizeWithdrawalApplicationPage, operationLabel, rechargeTypeLabel, statusLabel, validateApplicationPayload, + validateCoinSellerRechargeOrderPayload, walletIdentityLabel, } from "./format.js"; @@ -160,6 +165,84 @@ test("normalizes finance withdrawal application list rows", () => { }); }); +test("builds validates and normalizes coin seller recharge orders", () => { + const payload = buildCoinSellerRechargeOrderPayload({ + appCode: " lalu ", + coinAmount: "100000", + externalOrderNo: " MIFA-001 ", + providerCode: "mifapay", + remark: " 人工补单 ", + targetUserId: " 10001 ", + usdAmount: "10.5", + }); + + expect(payload).toEqual({ + appCode: "lalu", + coinAmount: 100000, + externalOrderNo: "MIFA-001", + providerCode: "mifapay", + remark: "人工补单", + targetUserId: "10001", + usdAmount: 10.5, + }); + expect( + buildCoinSellerRechargeReceiptPayload({ + ...payload, + coinAmount: "", + targetUserId: "", + }), + ).toEqual({ + appCode: "lalu", + externalOrderNo: "MIFA-001", + providerCode: "mifapay", + usdAmount: 10.5, + }); + expect(validateCoinSellerRechargeOrderPayload(payload)).toBe(""); + expect(validateCoinSellerRechargeOrderPayload({ ...payload, coinAmount: 0 })).toBe(""); + expect(validateCoinSellerRechargeOrderPayload({ ...payload, coinAmount: -1 })).toBe("请填写不小于 0 的整数金币"); + expect(validateCoinSellerRechargeOrderPayload({ ...payload, remark: "x".repeat(513) })).toBe("备注不能超过 512 字"); + expect(validateCoinSellerRechargeOrderPayload({ ...payload, providerCode: "usdt", chain: "" })).toBe( + "请选择 USDT 链", + ); + + const page = normalizeCoinSellerRechargeOrderPage({ + items: [ + { + app_code: "lalu", + coin_amount: 100000, + created_at_ms: 1760000000000, + external_order_no: "MIFA-001", + grant_status: "pending", + id: "order-1", + provider_code: "mifapay", + remark: "人工补单", + target_display_user_id: "165075", + target_user_id: "10001", + usd_amount: "10.50", + verify_status: "verified", + wallet_transaction_id: "wallet-tx", + }, + ], + page: 1, + page_size: 50, + total: 1, + }); + + expect(page.items[0]).toMatchObject({ + appCode: "lalu", + coinAmount: 100000, + externalOrderNo: "MIFA-001", + id: "order-1", + providerCode: "mifapay", + remark: "人工补单", + targetDisplayUserId: "165075", + targetUserId: "10001", + verifyStatus: "verified", + walletTransactionId: "wallet-tx", + }); + expect(coinSellerRechargeProviderLabel("mifapay")).toBe("MiFaPay"); +}); + test("normalizes app recharge detail list rows with optional payment fields", () => { const page = normalizeRechargeBillPage({ items: [ diff --git a/finance/src/styles/index.css b/finance/src/styles/index.css index e00d7b5..1a3cff3 100644 --- a/finance/src/styles/index.css +++ b/finance/src/styles/index.css @@ -386,6 +386,13 @@ a { font-size: 12px; } +.finance-remark-cell { + max-width: 180px; + color: var(--text-secondary); + white-space: normal; + word-break: break-word; +} + .finance-pagination__meta { color: var(--text-secondary); } @@ -528,6 +535,11 @@ a { border-radius: var(--radius-card) !important; } +.finance-create-dialog-paper--coin-seller { + width: min(760px, calc(100vw - 64px)); + max-width: min(760px, calc(100vw - 64px)) !important; +} + .finance-create-dialog-paper .MuiDialogTitle-root { padding: var(--space-5) var(--space-5) var(--space-2); font-size: 18px; @@ -544,6 +556,25 @@ a { gap: var(--space-3); } +.finance-form--dialog .finance-form-grid--coin-seller-create { + grid-template-columns: minmax(0, 1fr); +} + +.finance-form--dialog .finance-form-grid--coin-seller-no-chain { + grid-template-columns: minmax(0, 1fr); +} + +.finance-receipt-field { + display: grid; + min-width: 0; + grid-template-columns: minmax(0, 1fr) auto; + gap: var(--space-2); +} + +.finance-receipt-result { + align-items: center; +} + .finance-form--dialog .finance-evidence-grid { grid-template-columns: 320px minmax(0, 1fr); gap: var(--space-4); @@ -648,6 +679,8 @@ a { } .finance-form--dialog .finance-form-grid, + .finance-form--dialog .finance-form-grid--coin-seller-create, + .finance-form--dialog .finance-form-grid--coin-seller-no-chain, .finance-form--dialog .finance-evidence-grid { grid-template-columns: 1fr; } @@ -1072,6 +1105,10 @@ a { min-width: 1180px; } +.finance-flat-table--orders { + min-width: 1740px; +} + .finance-views { display: flex; gap: 2px; diff --git a/src/app/permissions.ts b/src/app/permissions.ts index 8f70dee..36f670a 100644 --- a/src/app/permissions.ts +++ b/src/app/permissions.ts @@ -47,6 +47,10 @@ export const PERMISSIONS = { financeApplicationAudit: "finance-application:audit", financeWithdrawalView: "finance-withdrawal:view", financeWithdrawalAudit: "finance-withdrawal:audit", + financeOrderCoinSellerRechargeView: "finance-order:coin-seller-recharge:view", + financeOrderCoinSellerRechargeCreate: "finance-order:coin-seller-recharge:create", + financeOrderCoinSellerRechargeVerify: "finance-order:coin-seller-recharge:verify", + financeOrderCoinSellerRechargeGrant: "finance-order:coin-seller-recharge:grant", financeOperationUserCoinCredit: "finance-operation:user-coin-credit", financeOperationUserCoinDebit: "finance-operation:user-coin-debit", financeOperationUserWalletCredit: "finance-operation:user-wallet-credit", diff --git a/src/shared/api/generated/endpoints.ts b/src/shared/api/generated/endpoints.ts index 1432da2..2f6da0a 100644 --- a/src/shared/api/generated/endpoints.ts +++ b/src/shared/api/generated/endpoints.ts @@ -45,6 +45,7 @@ export const API_OPERATIONS = { createEmojiPack: "createEmojiPack", createExploreTab: "createExploreTab", createFinanceApplication: "createFinanceApplication", + createFinanceCoinSellerRechargeOrder: "createFinanceCoinSellerRechargeOrder", createFullServerNoticeFanout: "createFullServerNoticeFanout", createGift: "createGift", createH5Link: "createH5Link", @@ -151,6 +152,7 @@ export const API_OPERATIONS = { getWeeklyStarCycle: "getWeeklyStarCycle", getWheelConfig: "getWheelConfig", getWheelDrawSummary: "getWheelDrawSummary", + grantFinanceCoinSellerRechargeOrder: "grantFinanceCoinSellerRechargeOrder", grantPrettyId: "grantPrettyId", grantResource: "grantResource", grantResourceGroup: "grantResourceGroup", @@ -177,6 +179,7 @@ export const API_OPERATIONS = { listEmojiPacks: "listEmojiPacks", listExploreTabs: "listExploreTabs", listFinanceApplications: "listFinanceApplications", + listFinanceCoinSellerRechargeOrders: "listFinanceCoinSellerRechargeOrders", listFinanceWithdrawalApplications: "listFinanceWithdrawalApplications", listFirstRechargeRewardClaims: "listFirstRechargeRewardClaims", listGifts: "listGifts", @@ -339,7 +342,9 @@ export const API_OPERATIONS = { upsertLuckyGiftConfig: "upsertLuckyGiftConfig", upsertResourceShopItems: "upsertResourceShopItems", upsertRule: "upsertRule", - upsertWheelConfig: "upsertWheelConfig" + upsertWheelConfig: "upsertWheelConfig", + verifyFinanceCoinSellerRechargeOrder: "verifyFinanceCoinSellerRechargeOrder", + verifyFinanceCoinSellerRechargeReceipt: "verifyFinanceCoinSellerRechargeReceipt" } as const; export type ApiOperationId = (typeof API_OPERATIONS)[keyof typeof API_OPERATIONS]; @@ -567,6 +572,13 @@ export const API_ENDPOINTS: Record = { permission: "finance-application:create", permissions: ["finance-application:create"] }, + createFinanceCoinSellerRechargeOrder: { + method: "POST", + operationId: API_OPERATIONS.createFinanceCoinSellerRechargeOrder, + path: "/v1/admin/finance/orders/coin-seller-recharges", + permission: "finance-order:coin-seller-recharge:create", + permissions: ["finance-order:coin-seller-recharge:create"] + }, createFullServerNoticeFanout: { method: "POST", operationId: API_OPERATIONS.createFullServerNoticeFanout, @@ -1309,6 +1321,13 @@ export const API_ENDPOINTS: Record = { permission: "wheel:view", permissions: ["wheel:view"] }, + grantFinanceCoinSellerRechargeOrder: { + method: "POST", + operationId: API_OPERATIONS.grantFinanceCoinSellerRechargeOrder, + path: "/v1/admin/finance/orders/coin-seller-recharges/{order_id}/grant", + permission: "finance-order:coin-seller-recharge:grant", + permissions: ["finance-order:coin-seller-recharge:grant"] + }, grantPrettyId: { method: "POST", operationId: API_OPERATIONS.grantPrettyId, @@ -1489,6 +1508,13 @@ export const API_ENDPOINTS: Record = { permission: "finance-application:audit", permissions: ["finance-application:audit"] }, + listFinanceCoinSellerRechargeOrders: { + method: "GET", + operationId: API_OPERATIONS.listFinanceCoinSellerRechargeOrders, + path: "/v1/admin/finance/orders/coin-seller-recharges", + permission: "finance-order:coin-seller-recharge:view", + permissions: ["finance-order:coin-seller-recharge:view"] + }, listFinanceWithdrawalApplications: { method: "GET", operationId: API_OPERATIONS.listFinanceWithdrawalApplications, @@ -2617,6 +2643,20 @@ export const API_ENDPOINTS: Record = { path: "/v1/admin/activity/wheel/config", permission: "wheel:update", permissions: ["wheel:update"] + }, + verifyFinanceCoinSellerRechargeOrder: { + method: "POST", + operationId: API_OPERATIONS.verifyFinanceCoinSellerRechargeOrder, + path: "/v1/admin/finance/orders/coin-seller-recharges/{order_id}/verify", + permission: "finance-order:coin-seller-recharge:verify", + permissions: ["finance-order:coin-seller-recharge:verify"] + }, + verifyFinanceCoinSellerRechargeReceipt: { + method: "POST", + operationId: API_OPERATIONS.verifyFinanceCoinSellerRechargeReceipt, + path: "/v1/admin/finance/orders/coin-seller-recharges/receipt-verifications", + permission: "finance-order:coin-seller-recharge:verify", + permissions: ["finance-order:coin-seller-recharge:verify"] } }; diff --git a/src/shared/api/generated/schema.d.ts b/src/shared/api/generated/schema.d.ts index a16f17e..5fcd923 100644 --- a/src/shared/api/generated/schema.d.ts +++ b/src/shared/api/generated/schema.d.ts @@ -2228,6 +2228,70 @@ export interface paths { patch?: never; trace?: never; }; + "/admin/finance/orders/coin-seller-recharges": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listFinanceCoinSellerRechargeOrders"]; + put?: never; + post: operations["createFinanceCoinSellerRechargeOrder"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/finance/orders/coin-seller-recharges/receipt-verifications": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["verifyFinanceCoinSellerRechargeReceipt"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/finance/orders/coin-seller-recharges/{order_id}/verify": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["verifyFinanceCoinSellerRechargeOrder"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/finance/orders/coin-seller-recharges/{order_id}/grant": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["grantFinanceCoinSellerRechargeOrder"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/admin/payment/temporary-links": { parameters: { query?: never; @@ -3890,6 +3954,12 @@ export interface components { pageSize: number; total: number; }; + ApiPageFinanceCoinSellerRechargeOrder: { + items: components["schemas"]["FinanceCoinSellerRechargeOrder"][]; + page: number; + pageSize: number; + total: number; + }; ApiPageFinanceWithdrawalApplication: { items: components["schemas"]["FinanceWithdrawalApplication"][]; page: number; @@ -3938,6 +4008,15 @@ export interface components { ApiResponseFinanceApplicationPage: components["schemas"]["Envelope"] & { data?: components["schemas"]["ApiPageFinanceApplication"]; }; + ApiResponseFinanceCoinSellerRechargeOrder: components["schemas"]["Envelope"] & { + data?: components["schemas"]["FinanceCoinSellerRechargeOrder"]; + }; + ApiResponseFinanceCoinSellerRechargeReceiptVerification: components["schemas"]["Envelope"] & { + data?: components["schemas"]["FinanceCoinSellerRechargeReceiptVerification"]; + }; + ApiResponseFinanceCoinSellerRechargeOrderPage: components["schemas"]["Envelope"] & { + data?: components["schemas"]["ApiPageFinanceCoinSellerRechargeOrder"]; + }; ApiResponseFinanceWithdrawalApplication: components["schemas"]["Envelope"] & { data?: components["schemas"]["FinanceWithdrawalApplication"]; }; @@ -4295,6 +4374,45 @@ export interface components { updatedAtMs: number; dingTalkNotified?: boolean; }; + FinanceCoinSellerRechargeOrder: { + id: string; + appCode: string; + targetUserId: string; + targetDisplayUserId?: string; + usdAmount: string; + usdMinorAmount?: number; + coinAmount: number; + /** @enum {string} */ + providerCode: "mifapay" | "v5pay" | "usdt"; + /** @enum {string} */ + chain?: "TRON" | "BSC"; + externalOrderNo: string; + providerCountryCode?: string; + providerCurrencyCode?: string; + providerAmountMinor?: number; + providerOrderId?: string; + providerStatus?: string; + providerPayType?: string; + receiveAddress?: string; + orderDate?: string; + status: string; + verifyStatus: string; + grantStatus: string; + remark?: string; + failureReason?: string; + providerPayloadJson?: string; + walletCommandId?: string; + walletTransactionId?: string; + walletAssetType?: string; + walletAmountDelta?: number; + walletBalanceAfter?: number; + operatorName?: string; + verifiedByName?: string; + grantedByName?: string; + createdAtMs: number; + verifiedAtMs?: number | null; + grantedAtMs?: number | null; + }; FinanceWithdrawalApplication: { id: number; appCode: string; @@ -4328,6 +4446,43 @@ export interface components { credentialImageUrl?: string; credentialText?: string; }; + FinanceCoinSellerRechargeOrderInput: { + appCode: string; + targetUserId: string; + usdAmount: number; + coinAmount: number; + /** @enum {string} */ + providerCode: "mifapay" | "v5pay" | "usdt"; + externalOrderNo: string; + /** @enum {string} */ + chain?: "TRON" | "BSC"; + remark?: string; + }; + FinanceCoinSellerRechargeReceiptVerification: { + verified: boolean; + providerCode?: string; + externalOrderNo?: string; + providerOrderId?: string; + status?: string; + currencyCode?: string; + providerAmountMinor?: number; + /** @enum {string} */ + chain?: "TRON" | "BSC"; + receiveAddress?: string; + verifiedAtMs?: number; + failureReason?: string; + rawJson?: string; + }; + FinanceCoinSellerRechargeReceiptVerificationInput: { + appCode: string; + /** @enum {string} */ + providerCode: "mifapay" | "v5pay" | "usdt"; + externalOrderNo: string; + /** @enum {string} */ + chain?: "TRON" | "BSC"; + usdAmount?: number; + usdMinorAmount?: number; + }; UserFinanceScope: { appCode: string; regionId: number; @@ -4425,6 +4580,33 @@ export interface components { }; }; /** @description OK */ + FinanceCoinSellerRechargeOrderPageResponse: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiResponseFinanceCoinSellerRechargeOrderPage"]; + }; + }; + /** @description OK */ + FinanceCoinSellerRechargeOrderResponse: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiResponseFinanceCoinSellerRechargeOrder"]; + }; + }; + /** @description OK */ + FinanceCoinSellerRechargeReceiptVerificationResponse: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiResponseFinanceCoinSellerRechargeReceiptVerification"]; + }; + }; + /** @description OK */ FinanceWithdrawalApplicationPageResponse: { headers: { [name: string]: unknown; @@ -4667,6 +4849,16 @@ export interface components { "application/json": components["schemas"]["FinanceApplicationInput"]; }; }; + FinanceCoinSellerRechargeOrderRequest: { + content: { + "application/json": components["schemas"]["FinanceCoinSellerRechargeOrderInput"]; + }; + }; + FinanceCoinSellerRechargeReceiptVerificationRequest: { + content: { + "application/json": components["schemas"]["FinanceCoinSellerRechargeReceiptVerificationInput"]; + }; + }; FinanceApplicationAuditRequest: { content: { "application/json": { @@ -7403,6 +7595,79 @@ export interface operations { 200: components["responses"]["FinanceWithdrawalApplicationResponse"]; }; }; + listFinanceCoinSellerRechargeOrders: { + parameters: { + query?: { + app_code?: string; + provider_code?: string; + status?: string; + verify_status?: string; + grant_status?: string; + keyword?: string; + page?: number; + page_size?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["FinanceCoinSellerRechargeOrderPageResponse"]; + }; + }; + createFinanceCoinSellerRechargeOrder: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: components["requestBodies"]["FinanceCoinSellerRechargeOrderRequest"]; + responses: { + 201: components["responses"]["FinanceCoinSellerRechargeOrderResponse"]; + }; + }; + verifyFinanceCoinSellerRechargeReceipt: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: components["requestBodies"]["FinanceCoinSellerRechargeReceiptVerificationRequest"]; + responses: { + 200: components["responses"]["FinanceCoinSellerRechargeReceiptVerificationResponse"]; + }; + }; + verifyFinanceCoinSellerRechargeOrder: { + parameters: { + query?: never; + header?: never; + path: { + order_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["FinanceCoinSellerRechargeOrderResponse"]; + }; + }; + grantFinanceCoinSellerRechargeOrder: { + parameters: { + query?: never; + header?: never; + path: { + order_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["FinanceCoinSellerRechargeOrderResponse"]; + }; + }; listTemporaryPaymentLinks: { parameters: { query?: never; diff --git a/src/shared/api/types.ts b/src/shared/api/types.ts index 6b46454..9e81a60 100644 --- a/src/shared/api/types.ts +++ b/src/shared/api/types.ts @@ -585,6 +585,79 @@ export interface TemporaryPaymentLinkDto { usdMinorAmount: number; } +export interface FinanceCoinSellerRechargeOrderDto { + appCode?: string; + chain?: string; + coinAmount: number; + createdAtMs?: number; + externalOrderNo?: string; + failureReason?: string; + grantStatus?: string; + grantedAtMs?: number; + grantedByName?: string; + id: EntityId; + operatorName?: string; + orderDate?: string; + providerAmountMinor?: number; + providerCode?: string; + providerCountryCode?: string; + providerCurrencyCode?: string; + providerOrderId?: string; + providerPayloadJson?: string; + providerPayType?: string; + providerStatus?: string; + receiveAddress?: string; + remark?: string; + status?: string; + targetDisplayUserId?: string; + targetUserId: string; + usdAmount?: number | string; + usdMinorAmount?: number; + verifiedAtMs?: number; + verifiedByName?: string; + verifyStatus?: string; + walletAmountDelta?: number; + walletAssetType?: string; + walletBalanceAfter?: number; + walletCommandId?: string; + walletTransactionId?: string; +} + +export interface FinanceCoinSellerRechargeOrderPayload { + appCode: string; + chain?: string; + coinAmount: number; + externalOrderNo: string; + providerCode: "mifapay" | "v5pay" | "usdt"; + remark?: string; + targetUserId: EntityId; + usdAmount: number; +} + +export interface FinanceCoinSellerRechargeReceiptVerificationPayload { + appCode: string; + chain?: string; + externalOrderNo: string; + providerCode: "mifapay" | "v5pay" | "usdt"; + usdAmount?: number; + usdMinorAmount?: number; +} + +export interface FinanceCoinSellerRechargeReceiptVerificationDto { + chain?: string; + currencyCode?: string; + externalOrderNo?: string; + failureReason?: string; + providerAmountMinor?: number; + providerCode?: string; + providerOrderId?: string; + rawJson?: string; + receiveAddress?: string; + status?: string; + verifiedAtMs?: number; + verified: boolean; +} + export interface ThirdPartyPaymentRateSyncDto { missingCurrencies?: string[]; rates?: Record;