diff --git a/contracts/admin-openapi.json b/contracts/admin-openapi.json index dc12785..e0b553a 100644 --- a/contracts/admin-openapi.json +++ b/contracts/admin-openapi.json @@ -1577,7 +1577,7 @@ "operationId": "listCoinSellers", "responses": { "200": { - "$ref": "#/components/responses/EmptyResponse" + "$ref": "#/components/responses/CoinSellerPageResponse" } }, "x-permission": "coin-seller:view", @@ -1585,9 +1585,12 @@ }, "post": { "operationId": "createCoinSeller", + "requestBody": { + "$ref": "#/components/requestBodies/CreateCoinSellerRequest" + }, "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" + "201": { + "$ref": "#/components/responses/CoinSellerResponse" } }, "x-permission": "coin-seller:create", @@ -1597,9 +1600,12 @@ "/admin/coin-sellers/{user_id}/status": { "patch": { "operationId": "setCoinSellerStatus", + "requestBody": { + "$ref": "#/components/requestBodies/CoinSellerStatusRequest" + }, "responses": { "200": { - "$ref": "#/components/responses/EmptyResponse" + "$ref": "#/components/responses/CoinSellerResponse" } }, "parameters": [ @@ -1770,9 +1776,18 @@ "/admin/countries": { "get": { "operationId": "listCountries", + "parameters": [ + { + "in": "query", + "name": "enabled", + "schema": { + "type": "boolean" + } + } + ], "responses": { "200": { - "$ref": "#/components/responses/EmptyResponse" + "$ref": "#/components/responses/CountryListResponse" } }, "x-permission": "country:view", @@ -1780,9 +1795,12 @@ }, "post": { "operationId": "createCountry", + "requestBody": { + "$ref": "#/components/requestBodies/CountryCreateRequest" + }, "responses": { - "200": { - "$ref": "#/components/responses/EmptyResponse" + "201": { + "$ref": "#/components/responses/CountryResponse" } }, "x-permission": "country:create", @@ -1794,7 +1812,7 @@ "operationId": "getCountry", "responses": { "200": { - "$ref": "#/components/responses/EmptyResponse" + "$ref": "#/components/responses/CountryResponse" } }, "parameters": [ @@ -1812,9 +1830,12 @@ }, "patch": { "operationId": "updateCountry", + "requestBody": { + "$ref": "#/components/requestBodies/CountryUpdateRequest" + }, "responses": { "200": { - "$ref": "#/components/responses/EmptyResponse" + "$ref": "#/components/responses/CountryResponse" } }, "parameters": [ @@ -1834,7 +1855,7 @@ "operationId": "deleteCountry", "responses": { "200": { - "$ref": "#/components/responses/EmptyResponse" + "$ref": "#/components/responses/CountryResponse" } }, "parameters": [ @@ -1851,12 +1872,37 @@ "x-permissions": ["country:status"] } }, + "/admin/countries/{country_id}/rename-code": { + "post": { + "operationId": "renameCountryCode", + "requestBody": { + "$ref": "#/components/requestBodies/CountryRenameCodeRequest" + }, + "responses": { + "201": { + "$ref": "#/components/responses/CountryCodeRenameJobResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "country_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "country:update", + "x-permissions": ["country:update"] + } + }, "/admin/countries/{country_id}/disable": { "post": { "operationId": "disableCountry", "responses": { "200": { - "$ref": "#/components/responses/EmptyResponse" + "$ref": "#/components/responses/CountryResponse" } }, "parameters": [ @@ -1878,7 +1924,7 @@ "operationId": "enableCountry", "responses": { "200": { - "$ref": "#/components/responses/EmptyResponse" + "$ref": "#/components/responses/CountryResponse" } }, "parameters": [ @@ -6038,6 +6084,70 @@ "x-permissions": ["user:export"] } }, + "/jobs": { + "get": { + "operationId": "listJobs", + "parameters": [ + { + "$ref": "#/components/parameters/Page" + }, + { + "$ref": "#/components/parameters/PageSize" + }, + { + "$ref": "#/components/parameters/Keyword" + }, + { + "in": "query", + "name": "status", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/AdminJobPageResponse" + } + }, + "x-permission": "job:view", + "x-permissions": ["job:view"] + } + }, + "/jobs/{id}": { + "get": { + "operationId": "getJob", + "parameters": [ + { + "$ref": "#/components/parameters/Id" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/AdminJobResponse" + } + }, + "x-permission": "job:view", + "x-permissions": ["job:view", "country:update"] + } + }, + "/jobs/{id}/cancel": { + "post": { + "operationId": "cancelJob", + "parameters": [ + { + "$ref": "#/components/parameters/Id" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "job:cancel", + "x-permissions": ["job:cancel"] + } + }, "/teams": { "get": { "operationId": "listTeams", @@ -6398,6 +6508,56 @@ } } }, + "CreateCoinSellerRequest": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCoinSellerPayload" + } + } + } + }, + "CoinSellerStatusRequest": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CoinSellerStatusPayload" + } + } + } + }, + "CountryCreateRequest": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CountryCreateInput" + } + } + } + }, + "CountryRenameCodeRequest": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CountryRenameCodeInput" + } + } + } + }, + "CountryUpdateRequest": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CountryUpdateInput" + } + } + } + }, "ChangePasswordRequest": { "content": { "application/json": { @@ -6681,6 +6841,26 @@ } } }, + "CoinSellerPageResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseCoinSellerPage" + } + } + } + }, + "CoinSellerResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseCoinSeller" + } + } + } + }, "BDProfilePageResponse": { "description": "OK", "content": { @@ -6691,6 +6871,56 @@ } } }, + "AdminJobPageResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseAdminJobPage" + } + } + } + }, + "AdminJobResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseAdminJob" + } + } + } + }, + "CountryCodeRenameJobResponse": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseCountryCodeRenameJob" + } + } + } + }, + "CountryListResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseCountryList" + } + } + } + }, + "CountryResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseCountry" + } + } + } + }, "CreateUserResponse": { "description": "OK", "content": { @@ -7004,6 +7234,27 @@ } } }, + "ApiPageAdminJob": { + "type": "object", + "required": ["items", "page", "pageSize", "total"], + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AdminJob" + } + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, "ApiPageManager": { "type": "object", "required": ["items", "page", "pageSize", "total"], @@ -7025,6 +7276,27 @@ } } }, + "ApiPageCoinSeller": { + "type": "object", + "required": ["items", "page", "pageSize", "total"], + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CoinSeller" + } + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, "ApiPageBDProfile": { "type": "object", "required": ["items", "page", "pageSize", "total"], @@ -7351,6 +7623,36 @@ } ] }, + "ApiResponseCoinSellerPage": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/ApiPageCoinSeller" + } + } + } + ] + }, + "ApiResponseCoinSeller": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/CoinSeller" + } + } + } + ] + }, "ApiResponseBDProfilePage": { "allOf": [ { @@ -7366,6 +7668,81 @@ } ] }, + "ApiResponseAdminJob": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/AdminJob" + } + } + } + ] + }, + "ApiResponseAdminJobPage": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/ApiPageAdminJob" + } + } + } + ] + }, + "ApiResponseCountry": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Country" + } + } + } + ] + }, + "ApiResponseCountryCodeRenameJob": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/CountryCodeRenameJob" + } + } + } + ] + }, + "ApiResponseCountryList": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/CountryList" + } + } + } + ] + }, "ApiResponseDashboardOverview": { "allOf": [ { @@ -7950,6 +8327,113 @@ } } }, + "CoinSeller": { + "type": "object", + "required": ["userId"], + "properties": { + "avatar": { + "type": "string" + }, + "canManageSubCoinSellers": { + "type": "boolean" + }, + "contact": { + "type": "string" + }, + "createdAtMs": { + "type": "integer" + }, + "createdByUserId": { + "type": "integer" + }, + "displayUserId": { + "type": "string" + }, + "merchantAssetType": { + "type": "string" + }, + "merchantBalance": { + "type": "integer" + }, + "prettyDisplayUserId": { + "type": "string" + }, + "prettyId": { + "type": "string" + }, + "regionId": { + "type": "integer" + }, + "regionName": { + "type": "string" + }, + "status": { + "type": "string", + "enum": ["active", "disabled"] + }, + "totalRechargeUsdtMicro": { + "type": "integer" + }, + "updatedAtMs": { + "type": "integer" + }, + "userId": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "CreateCoinSellerPayload": { + "type": "object", + "required": ["commandId", "targetUserId"], + "properties": { + "canManageSubCoinSellers": { + "type": "boolean" + }, + "commandId": { + "type": "string", + "maxLength": 120 + }, + "contact": { + "type": "string", + "maxLength": 128 + }, + "reason": { + "type": "string", + "maxLength": 200 + }, + "targetUserId": { + "type": "string" + } + } + }, + "CoinSellerStatusPayload": { + "type": "object", + "required": ["commandId", "status"], + "properties": { + "canManageSubCoinSellers": { + "type": "boolean" + }, + "commandId": { + "type": "string", + "maxLength": 120 + }, + "contact": { + "type": "string", + "maxLength": 128 + }, + "reason": { + "type": "string", + "maxLength": 200 + }, + "status": { + "type": "string", + "enum": ["active", "disabled"] + } + } + }, "CreateManagerPayload": { "type": "object", "required": ["targetUserId"], @@ -8345,6 +8829,206 @@ } ] }, + "AdminJob": { + "type": "object", + "required": ["id", "type", "status"], + "properties": { + "id": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "status": { + "type": "string" + }, + "payloadJson": { + "type": "string" + }, + "resultJson": { + "type": "string" + }, + "error": { + "type": "string" + }, + "attempt": { + "type": "integer" + }, + "maxAttempts": { + "type": "integer" + }, + "artifactPath": { + "type": "string" + }, + "createdBy": { + "type": "integer" + }, + "createdByName": { + "type": "string" + }, + "startedAtMs": { + "nullable": true, + "type": "integer" + }, + "finishedAtMs": { + "nullable": true, + "type": "integer" + }, + "createdAtMs": { + "type": "integer" + }, + "updatedAtMs": { + "type": "integer" + } + } + }, + "Country": { + "type": "object", + "required": ["countryCode", "countryId"], + "properties": { + "countryCode": { + "type": "string" + }, + "countryDisplayName": { + "type": "string" + }, + "countryId": { + "type": "integer" + }, + "countryName": { + "type": "string" + }, + "createdAtMs": { + "type": "integer" + }, + "enabled": { + "type": "boolean" + }, + "flag": { + "type": "string" + }, + "isoAlpha3": { + "type": "string" + }, + "isoNumeric": { + "type": "string" + }, + "phoneCountryCode": { + "type": "string" + }, + "sortOrder": { + "type": "integer" + }, + "updatedAtMs": { + "type": "integer" + } + } + }, + "CountryBaseInput": { + "type": "object", + "required": ["countryDisplayName", "countryName"], + "properties": { + "countryDisplayName": { + "type": "string" + }, + "countryName": { + "type": "string" + }, + "flag": { + "type": "string" + }, + "isoAlpha3": { + "type": "string" + }, + "isoNumeric": { + "type": "string" + }, + "phoneCountryCode": { + "type": "string" + }, + "sortOrder": { + "type": "integer" + } + } + }, + "CountryCreateInput": { + "allOf": [ + { + "$ref": "#/components/schemas/CountryBaseInput" + }, + { + "type": "object", + "required": ["countryCode"], + "properties": { + "countryCode": { + "type": "string" + }, + "enabled": { + "type": "boolean" + } + } + } + ] + }, + "CountryUpdateInput": { + "allOf": [ + { + "$ref": "#/components/schemas/CountryBaseInput" + } + ] + }, + "CountryRenameCodeInput": { + "allOf": [ + { + "$ref": "#/components/schemas/CountryBaseInput" + }, + { + "type": "object", + "required": ["countryCode"], + "properties": { + "countryCode": { + "type": "string" + } + } + } + ] + }, + "CountryCodeRenameJob": { + "type": "object", + "required": ["jobId", "status", "countryId", "oldCountryCode", "newCountryCode"], + "properties": { + "jobId": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "countryId": { + "type": "integer" + }, + "oldCountryCode": { + "type": "string" + }, + "newCountryCode": { + "type": "string" + } + } + }, + "CountryList": { + "type": "object", + "required": ["items", "total"], + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Country" + } + }, + "total": { + "type": "integer" + } + } + }, "Log": { "type": "object", "additionalProperties": true diff --git a/src/app/permissions.ts b/src/app/permissions.ts index 98a4492..04592ce 100644 --- a/src/app/permissions.ts +++ b/src/app/permissions.ts @@ -6,6 +6,8 @@ export const PERMISSIONS = { userStatus: "user:status", userResetPassword: "user:reset-password", userExport: "user:export", + jobView: "job:view", + jobCancel: "job:cancel", teamView: "team:view", teamCreate: "team:create", teamUpdate: "team:update", diff --git a/src/features/host-org/api.test.ts b/src/features/host-org/api.test.ts index f320681..18ef54f 100644 --- a/src/features/host-org/api.test.ts +++ b/src/features/host-org/api.test.ts @@ -18,6 +18,8 @@ import { listHosts, listManagers, listRegions, + getJob, + renameCountryCode, replaceRegionCountries, setCoinSellerStatus, updateBDLeaderPositionAlias, @@ -62,7 +64,13 @@ test("host org list APIs use generated admin paths and filters", async () => { region_id: 7, status: "active", }); - await createManager({ canBlockUser: false, canGrantBadge: false, canGrantVip: true, contact: "+63", targetUserId: "1003" }); + await createManager({ + canBlockUser: false, + canGrantBadge: false, + canGrantVip: true, + contact: "+63", + targetUserId: "1003", + }); await updateManager("1003", { canBlockUser: false, canGrantVehicle: true, @@ -212,6 +220,13 @@ test("country and region APIs use generated admin paths", async () => { enabled: true, }); await updateCountry(12, { countryDisplayName: "United States", countryName: "United States" }); + await renameCountryCode(12, { + countryCode: "USA", + countryDisplayName: "United States", + countryName: "United States", + sortOrder: 10, + }); + await getJob(99); await listRegions({ status: "active" }); await createRegion({ countries: ["US"], name: "North America", regionCode: "NA" }); await replaceRegionCountries(7, { countries: ["US", "CA"] }); @@ -220,10 +235,12 @@ test("country and region APIs use generated admin paths", async () => { const [countryListUrl, countryListInit] = vi.mocked(fetch).mock.calls[0]; const [countryCreateUrl, countryCreateInit] = vi.mocked(fetch).mock.calls[1]; const [countryUpdateUrl, countryUpdateInit] = vi.mocked(fetch).mock.calls[2]; - const [regionListUrl, regionListInit] = vi.mocked(fetch).mock.calls[3]; - const [regionCreateUrl, regionCreateInit] = vi.mocked(fetch).mock.calls[4]; - const [regionCountriesUrl, regionCountriesInit] = vi.mocked(fetch).mock.calls[5]; - const [regionDisableUrl, regionDisableInit] = vi.mocked(fetch).mock.calls[6]; + const [countryRenameUrl, countryRenameInit] = vi.mocked(fetch).mock.calls[3]; + const [jobUrl, jobInit] = vi.mocked(fetch).mock.calls[4]; + const [regionListUrl, regionListInit] = vi.mocked(fetch).mock.calls[5]; + const [regionCreateUrl, regionCreateInit] = vi.mocked(fetch).mock.calls[6]; + const [regionCountriesUrl, regionCountriesInit] = vi.mocked(fetch).mock.calls[7]; + const [regionDisableUrl, regionDisableInit] = vi.mocked(fetch).mock.calls[8]; expect(String(countryListUrl)).toContain("/api/v1/admin/countries?"); expect(String(countryListUrl)).toContain("enabled=true"); @@ -232,6 +249,12 @@ test("country and region APIs use generated admin paths", async () => { expect(countryCreateInit?.method).toBe("POST"); expect(String(countryUpdateUrl)).toContain("/api/v1/admin/countries/12"); expect(countryUpdateInit?.method).toBe("PATCH"); + expect(JSON.parse(String(countryUpdateInit?.body))).not.toHaveProperty("countryCode"); + expect(String(countryRenameUrl)).toContain("/api/v1/admin/countries/12/rename-code"); + expect(countryRenameInit?.method).toBe("POST"); + expect(JSON.parse(String(countryRenameInit?.body))).toMatchObject({ countryCode: "USA", sortOrder: 10 }); + expect(String(jobUrl)).toContain("/api/v1/jobs/99"); + expect(jobInit?.method).toBe("GET"); expect(String(regionListUrl)).toContain("/api/v1/admin/regions?"); expect(String(regionListUrl)).toContain("status=active"); expect(regionListInit?.method).toBe("GET"); diff --git a/src/features/host-org/api.ts b/src/features/host-org/api.ts index d769174..f780761 100644 --- a/src/features/host-org/api.ts +++ b/src/features/host-org/api.ts @@ -6,6 +6,7 @@ import type { AgencyHostAddPayload, AgencyJoinEnabledPayload, AgencyStatusPayload, + AdminJobDto, ApiList, ApiPage, BDLeaderPositionAliasPayload, @@ -19,8 +20,10 @@ import type { CoinSellerStockDebitDto, CoinSellerStockDebitPayload, CoinSellerStatusPayload, + CountryCodeRenameJobDto, CountryDto, CountryPayload, + CountryRenameCodePayload, CountryUpdatePayload, CreateAgencyPayload, CreateAgencyResultDto, @@ -78,6 +81,27 @@ export function updateCountry(countryId: EntityId, payload: CountryUpdatePayload ); } +export function renameCountryCode( + countryId: EntityId, + payload: CountryRenameCodePayload, +): Promise { + const endpoint = API_ENDPOINTS.renameCountryCode; + return apiRequest( + apiEndpointPath(API_OPERATIONS.renameCountryCode, { country_id: countryId }), + { + body: payload, + method: endpoint.method, + }, + ); +} + +export function getJob(jobId: EntityId): Promise { + const endpoint = API_ENDPOINTS.getJob; + return apiRequest(apiEndpointPath(API_OPERATIONS.getJob, { id: jobId }), { + method: endpoint.method, + }); +} + export function enableCountry(countryId: EntityId): Promise { const endpoint = API_ENDPOINTS.enableCountry; return apiRequest(apiEndpointPath(API_OPERATIONS.enableCountry, { country_id: countryId }), { @@ -192,10 +216,13 @@ export function createManager(payload: CreateManagerPayload): Promise { const endpoint = API_ENDPOINTS.updateManager; - return apiRequest(apiEndpointPath(API_OPERATIONS.updateManager, { user_id: userId }), { - body: payload, - method: endpoint.method, - }); + return apiRequest( + apiEndpointPath(API_OPERATIONS.updateManager, { user_id: userId }), + { + body: payload, + method: endpoint.method, + }, + ); } export function listHosts(query: PageQuery = {}): Promise> { diff --git a/src/features/host-org/hooks/useHostCoinSellersPage.js b/src/features/host-org/hooks/useHostCoinSellersPage.js index 4a02d36..23e1039 100644 --- a/src/features/host-org/hooks/useHostCoinSellersPage.js +++ b/src/features/host-org/hooks/useHostCoinSellersPage.js @@ -23,8 +23,15 @@ import { const pageSize = 50; const emptyData = { items: [], page: 1, pageSize, total: 0 }; -const emptyCreateForm = () => ({ commandId: makeCommandId("coin-seller"), contact: "", reason: "", targetUserId: "" }); +const emptyCreateForm = () => ({ + canManageSubCoinSellers: true, + commandId: makeCommandId("coin-seller"), + contact: "", + reason: "", + targetUserId: "", +}); const emptyEditForm = () => ({ + canManageSubCoinSellers: true, commandId: makeCommandId("coin-seller-status"), contact: "", reason: "", @@ -150,6 +157,7 @@ export function useHostCoinSellersPage() { } setSelectedSeller(seller); setEditForm({ + canManageSubCoinSellers: seller.canManageSubCoinSellers === true, commandId: makeCommandId("coin-seller-status"), contact: seller.contact || "", reason: "update coin seller contact", @@ -265,6 +273,7 @@ export function useHostCoinSellersPage() { `coin-seller-edit-${editForm.targetUserId || selectedSeller?.userId || ""}`, "币商已更新", async () => { + // 编辑接口复用 status 命令,提交时带上权限字段,避免只改联系方式时把子币商权限丢给后端默认值处理。 const payload = parseForm(coinSellerStatusSchema, editForm); const { targetUserId, ...body } = payload; await setCoinSellerStatus(targetUserId, body); @@ -286,7 +295,9 @@ export function useHostCoinSellersPage() { const sellerID = seller.userId; setContactSavingIds((current) => ({ ...current, [sellerID]: true })); try { + // 联系方式行内保存同样走状态接口,显式保留当前权限位,避免局部保存覆盖行级权限。 await setCoinSellerStatus(sellerID, { + canManageSubCoinSellers: seller.canManageSubCoinSellers === true, commandId: makeCommandId("coin-seller-contact"), contact, reason: "update coin seller contact", @@ -313,6 +324,37 @@ export function useHostCoinSellersPage() { } }; + const toggleSubCoinSellerPermission = async (seller, nextEnabled = !seller?.canManageSubCoinSellers) => { + if (!abilities.canUpdate || !seller?.userId) { + return; + } + if ((seller.canManageSubCoinSellers === true) === nextEnabled) { + return; + } + await runAction( + `coin-seller-sub-permission-${seller.userId}`, + nextEnabled ? "子币商权限已开启" : "子币商权限已关闭", + async () => { + // 子权限开关复用币商状态接口,必须保留 status/contact,防止权限更新被解释成状态重置。 + await setCoinSellerStatus(seller.userId, { + canManageSubCoinSellers: nextEnabled, + commandId: makeCommandId("coin-seller-sub-permission"), + contact: seller.contact || "", + reason: "update coin seller sub permission", + status: seller.status || "active", + }); + setSellerRowPatches((current) => ({ + ...current, + [seller.userId]: { + ...(current[seller.userId] || {}), + canManageSubCoinSellers: nextEnabled, + }, + })); + await reload(); + }, + ); + }; + const submitStockCredit = async (event) => { event.preventDefault(); if (!selectedSeller?.userId) { @@ -373,7 +415,9 @@ export function useHostCoinSellersPage() { return; } await runAction(`coin-seller-status-${seller.userId}`, nextEnabled ? "币商已启用" : "币商已停用", async () => { + // 状态开关只改变启停状态,显式保留权限位,避免后端按缺省值改写子币商授权。 await setCoinSellerStatus(seller.userId, { + canManageSubCoinSellers: seller.canManageSubCoinSellers === true, commandId: makeCommandId("coin-seller-status"), reason: "", status: nextStatus, @@ -444,6 +488,7 @@ export function useHostCoinSellersPage() { submitEditSeller, submitStockCredit, submitStockDebit, + toggleSubCoinSellerPermission, toggleSeller, updateRateTier, userFilter, diff --git a/src/features/host-org/hooks/useHostCountriesPage.js b/src/features/host-org/hooks/useHostCountriesPage.js index 45569cb..2cc3af2 100644 --- a/src/features/host-org/hooks/useHostCountriesPage.js +++ b/src/features/host-org/hooks/useHostCountriesPage.js @@ -2,9 +2,17 @@ import { useCallback, useMemo, useState } from "react"; import { parseForm } from "@/shared/forms/validation"; import { useAdminQuery } from "@/shared/hooks/useAdminQuery.js"; import { useToast } from "@/shared/ui/ToastProvider.jsx"; -import { createCountry, disableCountry, enableCountry, listCountries, updateCountry } from "@/features/host-org/api"; +import { + createCountry, + disableCountry, + enableCountry, + getJob, + listCountries, + renameCountryCode, + updateCountry, +} from "@/features/host-org/api"; import { useCountryAbilities } from "@/features/host-org/permissions.js"; -import { countryCreateSchema, countryUpdateSchema } from "@/features/host-org/schema"; +import { countryCreateSchema, countryRenameCodeSchema, countryUpdateSchema } from "@/features/host-org/schema"; const emptyCountryForm = () => ({ countryCode: "", @@ -86,9 +94,30 @@ export function useHostCountriesPage() { const submitCountry = async (event) => { event.preventDefault(); const isEdit = activeAction === "edit"; - await runAction(isEdit ? "country-edit" : "country-create", isEdit ? "国家已更新" : "国家已创建", async () => { - const payload = parseForm(isEdit ? countryUpdateSchema : countryCreateSchema, countryForm); - const data = isEdit ? await updateCountry(editingCountry.countryId, payload) : await createCountry(payload); + if (!isEdit) { + await runAction("country-create", "国家已创建", async () => { + const payload = parseForm(countryCreateSchema, countryForm); + const data = await createCountry(payload); + setCountryForm(emptyCountryForm()); + setEditingCountry(null); + setActiveAction(""); + await reload(); + return data; + }); + return; + } + + const currentCountryCode = normalizeCountryCode(editingCountry?.countryCode); + const nextCountryCode = normalizeCountryCode(countryForm.countryCode); + if (currentCountryCode !== nextCountryCode) { + await runCountryCodeRename(); + return; + } + + await runAction("country-edit", "国家已更新", async () => { + const { countryCode: _countryCode, enabled: _enabled, ...formWithoutImmutableFields } = countryForm; + const payload = parseForm(countryUpdateSchema, formWithoutImmutableFields); + const data = await updateCountry(editingCountry.countryId, payload); setCountryForm(emptyCountryForm()); setEditingCountry(null); setActiveAction(""); @@ -97,6 +126,26 @@ export function useHostCountriesPage() { }); }; + const runCountryCodeRename = async () => { + setLoadingAction("country-rename"); + try { + const payload = parseForm(countryRenameCodeSchema, countryForm); + const job = await renameCountryCode(editingCountry.countryId, payload); + showToast(`国家码重命名任务已创建:#${job.jobId}`, "success"); + setCountryForm(emptyCountryForm()); + setEditingCountry(null); + setActiveAction(""); + // 国家码会同步 user/admin/room 多个当前态,只有 job succeeded 后才刷新列表,避免短暂显示新旧码混杂。 + await waitCountryCodeRenameJob(job.jobId); + await reload(); + showToast("国家码重命名完成", "success"); + } catch (err) { + showToast(err.message || "国家码重命名失败", "error"); + } finally { + setLoadingAction(""); + } + }; + const toggleCountry = async (country, nextEnabled = !country.enabled) => { if (!abilities.canStatus || Boolean(country.enabled) === nextEnabled) { return; @@ -144,6 +193,33 @@ export function useHostCountriesPage() { }; } +async function waitCountryCodeRenameJob(jobId) { + const deadline = Date.now() + 120000; + while (Date.now() < deadline) { + const job = await getJob(jobId); + if (job.status === "succeeded") { + return job; + } + if (job.status === "failed" || job.status === "canceled") { + throw new Error(job.error || "国家码重命名任务失败"); + } + await delay(1500); + } + throw new Error("国家码重命名任务仍在处理中,请稍后刷新查看"); +} + +function delay(ms) { + return new Promise((resolve) => { + window.setTimeout(resolve, ms); + }); +} + +function normalizeCountryCode(value) { + return String(value || "") + .trim() + .toUpperCase(); +} + function filterCountries(items, query) { const keyword = query.trim().toLowerCase(); if (!keyword) { diff --git a/src/features/host-org/hooks/useHostCountriesPage.test.jsx b/src/features/host-org/hooks/useHostCountriesPage.test.jsx new file mode 100644 index 0000000..53c8288 --- /dev/null +++ b/src/features/host-org/hooks/useHostCountriesPage.test.jsx @@ -0,0 +1,127 @@ +import { act, renderHook } from "@testing-library/react"; +import { afterEach, expect, test, vi } from "vitest"; +import { useHostCountriesPage } from "./useHostCountriesPage.js"; +import { getJob, renameCountryCode, updateCountry } from "@/features/host-org/api"; +import { useCountryAbilities } from "@/features/host-org/permissions.js"; +import { useAdminQuery } from "@/shared/hooks/useAdminQuery.js"; +import { useToast } from "@/shared/ui/ToastProvider.jsx"; + +const mocks = vi.hoisted(() => ({ + reload: vi.fn(), + showToast: vi.fn(), +})); + +vi.mock("@/features/host-org/api", () => ({ + createCountry: vi.fn(), + disableCountry: vi.fn(), + enableCountry: vi.fn(), + getJob: vi.fn(), + listCountries: vi.fn(), + renameCountryCode: vi.fn(), + updateCountry: vi.fn(), +})); + +vi.mock("@/features/host-org/permissions.js", () => ({ + useCountryAbilities: vi.fn(), +})); + +vi.mock("@/shared/hooks/useAdminQuery.js", () => ({ + useAdminQuery: vi.fn(), +})); + +vi.mock("@/shared/ui/ToastProvider.jsx", () => ({ + useToast: vi.fn(), +})); + +afterEach(() => { + vi.clearAllMocks(); +}); + +test("country edit without code change uses metadata update endpoint", async () => { + mockHookDeps(); + vi.mocked(updateCountry).mockResolvedValue({ countryId: 12, countryCode: "PH" }); + const { result } = renderHook(() => useHostCountriesPage()); + + act(() => { + result.current.openEditCountry(countryFixture()); + }); + await act(async () => { + await result.current.submitCountry({ preventDefault: vi.fn() }); + }); + + expect(updateCountry).toHaveBeenCalledWith( + 12, + expect.not.objectContaining({ + countryCode: expect.anything(), + }), + ); + expect(renameCountryCode).not.toHaveBeenCalled(); + expect(mocks.reload).toHaveBeenCalled(); +}); + +test("country edit with code change creates rename job and polls until success", async () => { + mockHookDeps(); + vi.mocked(renameCountryCode).mockResolvedValue({ + countryId: 12, + jobId: 99, + newCountryCode: "PHL", + oldCountryCode: "PH", + status: "pending", + }); + vi.mocked(getJob).mockResolvedValue({ id: 99, status: "succeeded", type: "country-code-rename" }); + const { result } = renderHook(() => useHostCountriesPage()); + + act(() => { + result.current.openEditCountry(countryFixture()); + }); + act(() => { + result.current.setCountryForm({ ...result.current.countryForm, countryCode: "phl" }); + }); + await act(async () => { + await result.current.submitCountry({ preventDefault: vi.fn() }); + }); + + expect(renameCountryCode).toHaveBeenCalledWith( + 12, + expect.objectContaining({ + countryCode: "PHL", + countryDisplayName: "菲律宾", + countryName: "Philippines", + }), + ); + expect(getJob).toHaveBeenCalledWith(99); + expect(updateCountry).not.toHaveBeenCalled(); + expect(mocks.showToast).toHaveBeenCalledWith("国家码重命名完成", "success"); + expect(mocks.reload).toHaveBeenCalled(); +}); + +function mockHookDeps() { + vi.mocked(useCountryAbilities).mockReturnValue({ + canCreate: true, + canStatus: true, + canUpdate: true, + canView: true, + }); + vi.mocked(useToast).mockReturnValue({ showToast: mocks.showToast }); + vi.mocked(useAdminQuery).mockReturnValue({ + data: { items: [], total: 0 }, + error: null, + loading: false, + reload: mocks.reload, + }); +} + +function countryFixture() { + return { + countryCode: "PH", + countryDisplayName: "菲律宾", + countryId: 12, + countryName: "Philippines", + enabled: true, + flag: "🇵🇭", + isoAlpha3: "PHL", + isoNumeric: "608", + phoneCountryCode: "+63", + sortOrder: 1770, + }; +} diff --git a/src/features/host-org/pages/HostCoinSellersPage.jsx b/src/features/host-org/pages/HostCoinSellersPage.jsx index eb6f829..643f3ed 100644 --- a/src/features/host-org/pages/HostCoinSellersPage.jsx +++ b/src/features/host-org/pages/HostCoinSellersPage.jsx @@ -2,6 +2,7 @@ import Add from "@mui/icons-material/Add"; import ArrowDownwardOutlined from "@mui/icons-material/ArrowDownwardOutlined"; import ArrowUpwardOutlined from "@mui/icons-material/ArrowUpwardOutlined"; import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined"; +import EditOutlined from "@mui/icons-material/EditOutlined"; import MonetizationOnOutlined from "@mui/icons-material/MonetizationOnOutlined"; import RemoveCircleOutlineOutlined from "@mui/icons-material/RemoveCircleOutlineOutlined"; import ReceiptLongOutlined from "@mui/icons-material/ReceiptLongOutlined"; @@ -16,6 +17,7 @@ import { AdminFormFieldGrid, AdminFormReadOnlyField, AdminFormSection, + AdminFormSwitchField, } from "@/shared/ui/AdminFormDialog.jsx"; import { formatMillis } from "@/shared/utils/time.js"; import { DataState } from "@/shared/ui/DataState.jsx"; @@ -91,13 +93,19 @@ export function HostCoinSellersPage() { render: (item) => , width: "minmax(90px, 0.6fr)", }, + { + key: "canManageSubCoinSellers", + label: "子币商权限", + render: (item) => , + width: "minmax(120px, 0.7fr)", + }, ...baseColumns.slice(2), - page.abilities.canStockCredit || page.abilities.canLedger + page.abilities.canUpdate || page.abilities.canStockCredit || page.abilities.canLedger ? { key: "actions", label: "操作", render: (item) => , - width: "minmax(130px, 0.65fr)", + width: "minmax(160px, 0.75fr)", } : null, ] @@ -167,7 +175,7 @@ export function HostCoinSellersPage() { columns={columns} context={{ page, regionOptions: page.regionOptions }} items={items} - minWidth="930px" + minWidth="1080px" pagination={ total > 0 ? { @@ -208,6 +216,14 @@ export function HostCoinSellersPage() { value={page.createForm.contact} onChange={(event) => page.setCreateForm({ ...page.createForm, contact: event.target.value })} /> + + page.setCreateForm({ ...page.createForm, canManageSubCoinSellers: checked }) + } + /> page.setEditForm({ ...page.editForm, contact: event.target.value })} /> + page.setEditForm({ ...page.editForm, canManageSubCoinSellers: checked })} + /> page.toggleSubCoinSellerPermission(item, event.target.checked)} + /> + ); +} + function SellerContact({ item, page }) { const contact = String(item.contact || "").trim(); return ( @@ -505,6 +542,15 @@ function SellerContact({ item, page }) { function SellerActions({ item, page }) { return ( + {page.abilities.canUpdate ? ( + page.openEditSeller(item)} + > + + + ) : null} {page.abilities.canLedger ? ( ({ ledgerTable: vi.fn(), openSellerLedger: vi.fn(), + toggleSubCoinSellerPermission: vi.fn(), })); vi.mock("@/features/host-org/hooks/useHostCoinSellersPage.js", () => ({ @@ -41,9 +42,29 @@ test("coin seller table shows cumulative USDT and hides creator id column", () = expect(screen.queryByText("创建人 ID")).not.toBeInTheDocument(); expect(screen.getByText("累充USDT")).toBeInTheDocument(); + expect(screen.getByText("子币商权限")).toBeInTheDocument(); expect(screen.getByText("12.5")).toBeInTheDocument(); }); +test("coin seller sub permission switch toggles through the page hook", () => { + const seller = sellerFixture(); + vi.mocked(useHostCoinSellersPage).mockReturnValue( + pageFixture({ + abilities: { ...pageFixture().abilities, canUpdate: true }, + data: { items: [seller], page: 1, pageSize: 50, total: 1 }, + }), + ); + + render(); + + const switchInput = screen.getByRole("switch", { name: "关闭子币商权限" }); + expect(switchInput).toBeChecked(); + + fireEvent.click(switchInput); + + expect(mocks.toggleSubCoinSellerPermission).toHaveBeenCalledWith(seller, false); +}); + test("coin seller balance and cumulative USDT headers request sorting", () => { const changeSort = vi.fn(); vi.mocked(useHostCoinSellersPage).mockReturnValue(pageFixture({ changeSort })); @@ -86,6 +107,56 @@ test("coin seller ledger drawer passes locked seller id to shared table", () => expect(mocks.ledgerTable).toHaveBeenCalledWith(expect.objectContaining({ lockedSeller: seller, sellerUserId: "3001" })); }); +test("coin seller create form includes sub permission switch", () => { + const setCreateForm = vi.fn(); + vi.mocked(useHostCoinSellersPage).mockReturnValue( + pageFixture({ + abilities: { ...pageFixture().abilities, canCreate: true }, + activeAction: "create", + createForm: { canManageSubCoinSellers: true, contact: "", targetUserId: "" }, + setCreateForm, + }), + ); + + render(); + + const switchInput = screen.getByRole("switch", { name: "子币商权限" }); + expect(switchInput).toBeChecked(); + + fireEvent.click(switchInput); + + expect(setCreateForm).toHaveBeenCalledWith( + expect.objectContaining({ + canManageSubCoinSellers: false, + }), + ); +}); + +test("coin seller edit form preserves sub permission field", () => { + const setEditForm = vi.fn(); + vi.mocked(useHostCoinSellersPage).mockReturnValue( + pageFixture({ + abilities: { ...pageFixture().abilities, canUpdate: true }, + activeAction: "edit", + editForm: { canManageSubCoinSellers: false, contact: "+63", targetUserId: "3001" }, + setEditForm, + }), + ); + + render(); + + const switchInput = screen.getByRole("switch", { name: "子币商权限" }); + expect(switchInput).not.toBeChecked(); + + fireEvent.click(switchInput); + + expect(setEditForm).toHaveBeenCalledWith( + expect.objectContaining({ + canManageSubCoinSellers: true, + }), + ); +}); + function pageFixture(patch = {}) { return { abilities: { @@ -105,9 +176,9 @@ function pageFixture(patch = {}) { changeStatus: vi.fn(), closeAction: vi.fn(), contactSavingIds: {}, - createForm: { contact: "", targetUserId: "" }, + createForm: { canManageSubCoinSellers: true, contact: "", targetUserId: "" }, data: { items: [sellerFixture()], page: 1, pageSize: 50, total: 1 }, - editForm: { contact: "", targetUserId: "" }, + editForm: { canManageSubCoinSellers: true, contact: "", targetUserId: "" }, error: null, loading: false, loadingAction: "", @@ -142,6 +213,7 @@ function pageFixture(patch = {}) { submitEditSeller: vi.fn(), submitRateSettings: vi.fn(), submitStockCredit: vi.fn(), + toggleSubCoinSellerPermission: mocks.toggleSubCoinSellerPermission, toggleSeller: vi.fn(), updateRateTier: vi.fn(), ...patch, @@ -151,6 +223,7 @@ function pageFixture(patch = {}) { function sellerFixture() { return { avatar: "", + canManageSubCoinSellers: true, contact: "菲律賓 +63", displayUserId: "164425", merchantAssetType: "COIN_SELLER_COIN", diff --git a/src/features/host-org/pages/HostCountriesPage.jsx b/src/features/host-org/pages/HostCountriesPage.jsx index 3bf772e..52113d4 100644 --- a/src/features/host-org/pages/HostCountriesPage.jsx +++ b/src/features/host-org/pages/HostCountriesPage.jsx @@ -108,7 +108,11 @@ export function HostCountriesPage() { ({ + setCountryForm: vi.fn(), +})); + +vi.mock("@/features/host-org/hooks/useHostCountriesPage.js", () => ({ + useHostCountriesPage: vi.fn(), +})); + +afterEach(() => { + vi.clearAllMocks(); +}); + +test("country code field is editable when editing a country with update permission", () => { + vi.mocked(useHostCountriesPage).mockReturnValue(pageFixture()); + + render(); + + const countryCodeInput = screen.getByRole("textbox", { name: /国家码/ }); + expect(countryCodeInput).not.toBeDisabled(); + + fireEvent.change(countryCodeInput, { target: { value: "PHL" } }); + + expect(mocks.setCountryForm).toHaveBeenCalledWith(expect.objectContaining({ countryCode: "PHL" })); +}); + +test("country code field stays disabled when editing without update permission", () => { + vi.mocked(useHostCountriesPage).mockReturnValue( + pageFixture({ + abilities: { ...pageFixture().abilities, canUpdate: false }, + }), + ); + + render(); + + expect(screen.getByRole("textbox", { name: /国家码/ })).toBeDisabled(); +}); + +function pageFixture(patch = {}) { + return { + abilities: { + canCreate: true, + canStatus: true, + canUpdate: true, + canView: true, + }, + activeAction: "edit", + changeEnabledStatus: vi.fn(), + changeQuery: vi.fn(), + closeAction: vi.fn(), + countryForm: countryFormFixture(), + data: { items: [], total: 0 }, + editingCountry: countryFixture(), + enabledStatus: "", + error: null, + loading: false, + loadingAction: "", + openCreateCountry: vi.fn(), + openEditCountry: vi.fn(), + query: "", + reload: vi.fn(), + resetFilters: vi.fn(), + setCountryForm: mocks.setCountryForm, + submitCountry: vi.fn(), + toggleCountry: vi.fn(), + ...patch, + }; +} + +function countryFormFixture() { + return { + countryCode: "PH", + countryDisplayName: "菲律宾", + countryName: "Philippines", + enabled: true, + flag: "🇵🇭", + isoAlpha3: "PHL", + isoNumeric: "608", + phoneCountryCode: "+63", + sortOrder: 1770, + }; +} + +function countryFixture() { + return { + ...countryFormFixture(), + countryId: 12, + updatedAtMs: 1783578249000, + }; +} diff --git a/src/features/host-org/schema.test.ts b/src/features/host-org/schema.test.ts index 2104d1c..bf3d8e5 100644 --- a/src/features/host-org/schema.test.ts +++ b/src/features/host-org/schema.test.ts @@ -1,5 +1,13 @@ import { expect, test } from "vitest"; -import { agencyCloseSchema, agencyJoinEnabledSchema, coinSellerStatusSchema, createManagerSchema } from "./schema"; +import { + agencyCloseSchema, + agencyJoinEnabledSchema, + coinSellerStatusSchema, + createCoinSellerSchema, + createManagerSchema, + countryRenameCodeSchema, + countryUpdateSchema, +} from "./schema"; test("coin seller contact accepts plus sign with large internal user id", () => { const result = coinSellerStatusSchema.safeParse({ @@ -17,6 +25,37 @@ test("coin seller contact accepts plus sign with large internal user id", () => } }); +test("coin seller schemas preserve sub seller permission defaults and explicit false", () => { + const createDefault = createCoinSellerSchema.safeParse({ + commandId: "coin-seller-create-test", + targetUserId: "1003", + }); + const statusDefault = coinSellerStatusSchema.safeParse({ + commandId: "coin-seller-edit-test", + status: "active", + targetUserId: "320756743338463232", + }); + const statusDisabled = coinSellerStatusSchema.safeParse({ + canManageSubCoinSellers: false, + commandId: "coin-seller-edit-test", + status: "active", + targetUserId: "320756743338463232", + }); + + expect(createDefault.success).toBe(true); + expect(statusDefault.success).toBe(true); + expect(statusDisabled.success).toBe(true); + if (createDefault.success) { + expect(createDefault.data.canManageSubCoinSellers).toBe(true); + } + if (statusDefault.success) { + expect(statusDefault.data).not.toHaveProperty("canManageSubCoinSellers"); + } + if (statusDisabled.success) { + expect(statusDisabled.data.canManageSubCoinSellers).toBe(false); + } +}); + test("agency action schemas preserve large agency ids as strings", () => { const agencyId = "321170072154411009"; const closeResult = agencyCloseSchema.safeParse({ @@ -41,6 +80,36 @@ test("agency action schemas preserve large agency ids as strings", () => { } }); +test("country update schema excludes country code from metadata update payload", () => { + const result = countryUpdateSchema.safeParse({ + countryCode: "phl", + countryDisplayName: "Philippines Display", + countryName: "Philippines", + sortOrder: "1770", + }); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).not.toHaveProperty("countryCode"); + expect(result.data).not.toHaveProperty("enabled"); + } +}); + +test("country rename schema keeps editable country code", () => { + const result = countryRenameCodeSchema.safeParse({ + countryCode: "phl", + countryDisplayName: "Philippines Display", + countryName: "Philippines", + sortOrder: "1770", + }); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.countryCode).toBe("PHL"); + expect(result.data).not.toHaveProperty("enabled"); + } +}); + test("manager schema defaults vip grant permission on and preserves explicit false", () => { const defaultResult = createManagerSchema.safeParse({ targetUserId: "1003" }); const disabledResult = createManagerSchema.safeParse({ canGrantVip: false, targetUserId: "1003" }); diff --git a/src/features/host-org/schema.ts b/src/features/host-org/schema.ts index 2aa2f78..47fd81c 100644 --- a/src/features/host-org/schema.ts +++ b/src/features/host-org/schema.ts @@ -105,6 +105,7 @@ export const bdStatusSchema = commandBaseSchema.extend({ }); export const createCoinSellerSchema = commandContactSchema.extend({ + canManageSubCoinSellers: z.boolean().optional().default(true), targetUserId: userIdSchema, }); @@ -124,6 +125,7 @@ export const createManagerSchema = z.object({ }); export const coinSellerStatusSchema = commandContactSchema.extend({ + canManageSubCoinSellers: z.boolean().optional(), status: z.enum(["active", "disabled"]), targetUserId: entityIdSchema, }); @@ -200,6 +202,10 @@ export const countryUpdateSchema = countryCreateSchema.omit({ enabled: true, }); +export const countryRenameCodeSchema = countryCreateSchema.omit({ + enabled: true, +}); + export const regionCreateSchema = regionBaseSchema; export const regionUpdateSchema = regionBaseSchema; @@ -221,6 +227,7 @@ export type AgencyCloseForm = z.infer; export type AgencyJoinEnabledForm = z.infer; export type CountryCreateForm = z.infer; export type CountryUpdateForm = z.infer; +export type CountryRenameCodeForm = z.infer; export type RegionCreateForm = z.infer; export type RegionUpdateForm = z.infer; export type RegionCountriesForm = z.infer; diff --git a/src/shared/api/generated/endpoints.ts b/src/shared/api/generated/endpoints.ts index f6e5f55..e096728 100644 --- a/src/shared/api/generated/endpoints.ts +++ b/src/shared/api/generated/endpoints.ts @@ -29,6 +29,7 @@ export const API_OPERATIONS = { batchCreateGifts: "batchCreateGifts", batchDeleteResources: "batchDeleteResources", batchUpdateUserStatus: "batchUpdateUserStatus", + cancelJob: "cancelJob", cancelRoomPin: "cancelRoomPin", changePassword: "changePassword", closeAgency: "closeAgency", @@ -124,6 +125,7 @@ export const API_OPERATIONS = { getFirstRechargeRewardConfig: "getFirstRechargeRewardConfig", getHumanRoomRobotConfig: "getHumanRoomRobotConfig", getInviteActivityRewardConfig: "getInviteActivityRewardConfig", + getJob: "getJob", getLuckyGiftConfig: "getLuckyGiftConfig", getLuckyGiftDrawSummary: "getLuckyGiftDrawSummary", getRechargeBillOverview: "getRechargeBillOverview", @@ -191,6 +193,7 @@ export const API_OPERATIONS = { listHostSalarySettlements: "listHostSalarySettlements", listHostWithdrawals: "listHostWithdrawals", listInviteActivityRewardClaims: "listInviteActivityRewardClaims", + listJobs: "listJobs", listLevelConfig: "listLevelConfig", listLoginLogs: "listLoginLogs", listLuckyGiftConfigs: "listLuckyGiftConfigs", @@ -257,6 +260,7 @@ export const API_OPERATIONS = { refreshGoogleRechargePaid: "refreshGoogleRechargePaid", rejectFinanceApplication: "rejectFinanceApplication", rejectFinanceWithdrawalApplication: "rejectFinanceWithdrawalApplication", + renameCountryCode: "renameCountryCode", replaceCoinSellerSalaryRates: "replaceCoinSellerSalaryRates", replaceRegionBlocks: "replaceRegionBlocks", replaceRegionCountries: "replaceRegionCountries", @@ -468,6 +472,13 @@ export const API_ENDPOINTS: Record = { permission: "user:status", permissions: ["user:status"] }, + cancelJob: { + method: "POST", + operationId: API_OPERATIONS.cancelJob, + path: "/v1/jobs/{id}/cancel", + permission: "job:cancel", + permissions: ["job:cancel"] + }, cancelRoomPin: { method: "DELETE", operationId: API_OPERATIONS.cancelRoomPin, @@ -1131,6 +1142,13 @@ export const API_ENDPOINTS: Record = { permission: "invite-activity-reward:view", permissions: ["invite-activity-reward:view"] }, + getJob: { + method: "GET", + operationId: API_OPERATIONS.getJob, + path: "/v1/jobs/{id}", + permission: "job:view", + permissions: ["job:view","country:update"] + }, getLuckyGiftConfig: { method: "GET", operationId: API_OPERATIONS.getLuckyGiftConfig, @@ -1598,6 +1616,13 @@ export const API_ENDPOINTS: Record = { permission: "invite-activity-reward:view", permissions: ["invite-activity-reward:view"] }, + listJobs: { + method: "GET", + operationId: API_OPERATIONS.listJobs, + path: "/v1/jobs", + permission: "job:view", + permissions: ["job:view"] + }, listLevelConfig: { method: "GET", operationId: API_OPERATIONS.listLevelConfig, @@ -2050,6 +2075,13 @@ export const API_ENDPOINTS: Record = { permission: "finance-withdrawal:audit", permissions: ["finance-withdrawal:audit","finance-application:audit"] }, + renameCountryCode: { + method: "POST", + operationId: API_OPERATIONS.renameCountryCode, + path: "/v1/admin/countries/{country_id}/rename-code", + permission: "country:update", + permissions: ["country:update"] + }, replaceCoinSellerSalaryRates: { method: "PUT", operationId: API_OPERATIONS.replaceCoinSellerSalaryRates, diff --git a/src/shared/api/generated/schema.d.ts b/src/shared/api/generated/schema.d.ts index fcb6520..f9a1a6a 100644 --- a/src/shared/api/generated/schema.d.ts +++ b/src/shared/api/generated/schema.d.ts @@ -1204,6 +1204,22 @@ export interface paths { patch: operations["updateCountry"]; trace?: never; }; + "/admin/countries/{country_id}/rename-code": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["renameCountryCode"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/admin/countries/{country_id}/disable": { parameters: { query?: never; @@ -3812,6 +3828,54 @@ export interface paths { patch?: never; trace?: never; }; + "/jobs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listJobs"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/jobs/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getJob"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/jobs/{id}/cancel": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["cancelJob"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/teams": { parameters: { query?: never; @@ -3982,12 +4046,24 @@ export interface components { pageSize: number; total: number; }; + ApiPageAdminJob: { + items: components["schemas"]["AdminJob"][]; + page: number; + pageSize: number; + total: number; + }; ApiPageManager: { items: components["schemas"]["Manager"][]; page: number; pageSize: number; total: number; }; + ApiPageCoinSeller: { + items: components["schemas"]["CoinSeller"][]; + page: number; + pageSize: number; + total: number; + }; ApiPageBDProfile: { items: components["schemas"]["BDProfile"][]; page: number; @@ -4135,9 +4211,30 @@ export interface components { ApiResponseManager: components["schemas"]["Envelope"] & { data?: components["schemas"]["Manager"]; }; + ApiResponseCoinSellerPage: components["schemas"]["Envelope"] & { + data?: components["schemas"]["ApiPageCoinSeller"]; + }; + ApiResponseCoinSeller: components["schemas"]["Envelope"] & { + data?: components["schemas"]["CoinSeller"]; + }; ApiResponseBDProfilePage: components["schemas"]["Envelope"] & { data?: components["schemas"]["ApiPageBDProfile"]; }; + ApiResponseAdminJob: components["schemas"]["Envelope"] & { + data?: components["schemas"]["AdminJob"]; + }; + ApiResponseAdminJobPage: components["schemas"]["Envelope"] & { + data?: components["schemas"]["ApiPageAdminJob"]; + }; + ApiResponseCountry: components["schemas"]["Envelope"] & { + data?: components["schemas"]["Country"]; + }; + ApiResponseCountryCodeRenameJob: components["schemas"]["Envelope"] & { + data?: components["schemas"]["CountryCodeRenameJob"]; + }; + ApiResponseCountryList: components["schemas"]["Envelope"] & { + data?: components["schemas"]["CountryList"]; + }; ApiResponseDashboardOverview: components["schemas"]["Envelope"] & { data?: components["schemas"]["DashboardOverview"]; }; @@ -4275,6 +4372,41 @@ export interface components { userId: string; username?: string; }; + CoinSeller: { + avatar?: string; + canManageSubCoinSellers?: boolean; + contact?: string; + createdAtMs?: number; + createdByUserId?: number; + displayUserId?: string; + merchantAssetType?: string; + merchantBalance?: number; + prettyDisplayUserId?: string; + prettyId?: string; + regionId?: number; + regionName?: string; + /** @enum {string} */ + status?: "active" | "disabled"; + totalRechargeUsdtMicro?: number; + updatedAtMs?: number; + userId: string; + username?: string; + }; + CreateCoinSellerPayload: { + canManageSubCoinSellers?: boolean; + commandId: string; + contact?: string; + reason?: string; + targetUserId: string; + }; + CoinSellerStatusPayload: { + canManageSubCoinSellers?: boolean; + commandId: string; + contact?: string; + reason?: string; + /** @enum {string} */ + status: "active" | "disabled"; + }; CreateManagerPayload: { canAddAdmin?: boolean; canAddBdLeader?: boolean; @@ -4406,6 +4538,65 @@ export interface components { updatedByAdminId?: number; }; HumanRoomRobotConfigInput: components["schemas"]["HumanRoomRobotConfig"]; + AdminJob: { + id: number; + type: string; + status: string; + payloadJson?: string; + resultJson?: string; + error?: string; + attempt?: number; + maxAttempts?: number; + artifactPath?: string; + createdBy?: number; + createdByName?: string; + startedAtMs?: number | null; + finishedAtMs?: number | null; + createdAtMs?: number; + updatedAtMs?: number; + }; + Country: { + countryCode: string; + countryDisplayName?: string; + countryId: number; + countryName?: string; + createdAtMs?: number; + enabled?: boolean; + flag?: string; + isoAlpha3?: string; + isoNumeric?: string; + phoneCountryCode?: string; + sortOrder?: number; + updatedAtMs?: number; + }; + CountryBaseInput: { + countryDisplayName: string; + countryName: string; + flag?: string; + isoAlpha3?: string; + isoNumeric?: string; + phoneCountryCode?: string; + sortOrder?: number; + }; + CountryCreateInput: components["schemas"]["CountryBaseInput"] & { + countryCode: string; + enabled?: boolean; + }; + CountryUpdateInput: components["schemas"]["CountryBaseInput"]; + CountryRenameCodeInput: components["schemas"]["CountryBaseInput"] & { + countryCode: string; + }; + CountryCodeRenameJob: { + jobId: number; + status: string; + countryId: number; + oldCountryCode: string; + newCountryCode: string; + }; + CountryList: { + items: components["schemas"]["Country"][]; + total: number; + }; Log: { [key: string]: unknown; }; @@ -4662,6 +4853,24 @@ export interface components { }; }; /** @description OK */ + CoinSellerPageResponse: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiResponseCoinSellerPage"]; + }; + }; + /** @description OK */ + CoinSellerResponse: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiResponseCoinSeller"]; + }; + }; + /** @description OK */ BDProfilePageResponse: { headers: { [name: string]: unknown; @@ -4671,6 +4880,51 @@ export interface components { }; }; /** @description OK */ + AdminJobPageResponse: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiResponseAdminJobPage"]; + }; + }; + /** @description OK */ + AdminJobResponse: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiResponseAdminJob"]; + }; + }; + /** @description Created */ + CountryCodeRenameJobResponse: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiResponseCountryCodeRenameJob"]; + }; + }; + /** @description OK */ + CountryListResponse: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiResponseCountryList"]; + }; + }; + /** @description OK */ + CountryResponse: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiResponseCountry"]; + }; + }; + /** @description OK */ CreateUserResponse: { headers: { [name: string]: unknown; @@ -4966,6 +5220,31 @@ export interface components { }; }; }; + CreateCoinSellerRequest: { + content: { + "application/json": components["schemas"]["CreateCoinSellerPayload"]; + }; + }; + CoinSellerStatusRequest: { + content: { + "application/json": components["schemas"]["CoinSellerStatusPayload"]; + }; + }; + CountryCreateRequest: { + content: { + "application/json": components["schemas"]["CountryCreateInput"]; + }; + }; + CountryRenameCodeRequest: { + content: { + "application/json": components["schemas"]["CountryRenameCodeInput"]; + }; + }; + CountryUpdateRequest: { + content: { + "application/json": components["schemas"]["CountryUpdateInput"]; + }; + }; ChangePasswordRequest: { content: { "application/json": { @@ -6417,7 +6696,7 @@ export interface operations { }; requestBody?: never; responses: { - 200: components["responses"]["EmptyResponse"]; + 200: components["responses"]["CoinSellerPageResponse"]; }; }; createCoinSeller: { @@ -6427,9 +6706,9 @@ export interface operations { path?: never; cookie?: never; }; - requestBody?: never; + requestBody: components["requestBodies"]["CreateCoinSellerRequest"]; responses: { - 200: components["responses"]["EmptyResponse"]; + 201: components["responses"]["CoinSellerResponse"]; }; }; setCoinSellerStatus: { @@ -6441,9 +6720,9 @@ export interface operations { }; cookie?: never; }; - requestBody?: never; + requestBody: components["requestBodies"]["CoinSellerStatusRequest"]; responses: { - 200: components["responses"]["EmptyResponse"]; + 200: components["responses"]["CoinSellerResponse"]; }; }; creditCoinSellerStock: { @@ -6526,14 +6805,16 @@ export interface operations { }; listCountries: { parameters: { - query?: never; + query?: { + enabled?: boolean; + }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { - 200: components["responses"]["EmptyResponse"]; + 200: components["responses"]["CountryListResponse"]; }; }; createCountry: { @@ -6543,9 +6824,9 @@ export interface operations { path?: never; cookie?: never; }; - requestBody?: never; + requestBody: components["requestBodies"]["CountryCreateRequest"]; responses: { - 200: components["responses"]["EmptyResponse"]; + 201: components["responses"]["CountryResponse"]; }; }; getCountry: { @@ -6559,7 +6840,7 @@ export interface operations { }; requestBody?: never; responses: { - 200: components["responses"]["EmptyResponse"]; + 200: components["responses"]["CountryResponse"]; }; }; deleteCountry: { @@ -6573,7 +6854,7 @@ export interface operations { }; requestBody?: never; responses: { - 200: components["responses"]["EmptyResponse"]; + 200: components["responses"]["CountryResponse"]; }; }; updateCountry: { @@ -6585,9 +6866,23 @@ export interface operations { }; cookie?: never; }; - requestBody?: never; + requestBody: components["requestBodies"]["CountryUpdateRequest"]; responses: { - 200: components["responses"]["EmptyResponse"]; + 200: components["responses"]["CountryResponse"]; + }; + }; + renameCountryCode: { + parameters: { + query?: never; + header?: never; + path: { + country_id: number; + }; + cookie?: never; + }; + requestBody: components["requestBodies"]["CountryRenameCodeRequest"]; + responses: { + 201: components["responses"]["CountryCodeRenameJobResponse"]; }; }; disableCountry: { @@ -6601,7 +6896,7 @@ export interface operations { }; requestBody?: never; responses: { - 200: components["responses"]["EmptyResponse"]; + 200: components["responses"]["CountryResponse"]; }; }; enableCountry: { @@ -6615,7 +6910,7 @@ export interface operations { }; requestBody?: never; responses: { - 200: components["responses"]["EmptyResponse"]; + 200: components["responses"]["CountryResponse"]; }; }; listEmojiPacks: { @@ -9658,6 +9953,51 @@ export interface operations { }; }; }; + listJobs: { + parameters: { + query?: { + page?: components["parameters"]["Page"]; + page_size?: components["parameters"]["PageSize"]; + keyword?: components["parameters"]["Keyword"]; + status?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["AdminJobPageResponse"]; + }; + }; + getJob: { + parameters: { + query?: never; + header?: never; + path: { + id: components["parameters"]["Id"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["AdminJobResponse"]; + }; + }; + cancelJob: { + parameters: { + query?: never; + header?: never; + path: { + id: components["parameters"]["Id"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["EmptyResponse"]; + }; + }; listTeams: { parameters: { query?: { diff --git a/src/shared/api/types.ts b/src/shared/api/types.ts index 9e81a60..c439833 100644 --- a/src/shared/api/types.ts +++ b/src/shared/api/types.ts @@ -830,6 +830,7 @@ export interface HostProfileDto { export interface CoinSellerDto { avatar?: string; + canManageSubCoinSellers?: boolean; contact?: string; createdAtMs?: number; createdByUserId?: number; @@ -1609,11 +1610,13 @@ export interface BDStatusPayload extends HostCommandPayload { } export interface CreateCoinSellerPayload extends HostCommandPayload { + canManageSubCoinSellers?: boolean; contact?: string; targetUserId: string; } export interface CoinSellerStatusPayload extends BDStatusPayload { + canManageSubCoinSellers?: boolean; contact?: string; } @@ -1652,7 +1655,7 @@ export interface CountryDto { } export interface CountryPayload { - countryCode?: string; + countryCode: string; countryDisplayName: string; countryName: string; enabled?: boolean; @@ -1665,6 +1668,34 @@ export interface CountryPayload { export type CountryUpdatePayload = Omit; +export type CountryRenameCodePayload = Omit; + +export interface CountryCodeRenameJobDto { + countryId: number; + jobId: number; + newCountryCode: string; + oldCountryCode: string; + status: string; +} + +export interface AdminJobDto { + artifactPath?: string; + attempt?: number; + createdAtMs?: number; + createdBy?: number; + createdByName?: string; + error?: string; + finishedAtMs?: number | null; + id: number; + maxAttempts?: number; + payloadJson?: string; + resultJson?: string; + startedAtMs?: number | null; + status: string; + type: string; + updatedAtMs?: number; +} + export interface RegionDto { countries?: string[]; createdAtMs?: number;