From 44f95038b04e9c6f4e44b243a907a9fd93d96786 Mon Sep 17 00:00:00 2001 From: heheer Date: Mon, 8 Dec 2025 18:07:31 +0800 Subject: [PATCH] bill coupon detail (#6054) * bill coupon detail * enum * fix --- .../global/openapi/support/wallet/bill/api.ts | 25 +++- .../support/wallet/sub/coupon/constants.ts | 2 + packages/service/support/wallet/sub/utils.ts | 42 +++---- packages/web/i18n/en/account.json | 17 ++- packages/web/i18n/zh-CN/account.json | 45 ++++--- packages/web/i18n/zh-Hant/account.json | 63 ++++++---- projects/app/src/components/Layout/navbar.tsx | 2 +- .../wallet/StandardPlanContentList.tsx | 22 ++-- .../account/bill/BillDetailModal.tsx | 110 +++++++++++++++++- 9 files changed, 247 insertions(+), 81 deletions(-) diff --git a/packages/global/openapi/support/wallet/bill/api.ts b/packages/global/openapi/support/wallet/bill/api.ts index 59e9e577fd..3e746eecbb 100644 --- a/packages/global/openapi/support/wallet/bill/api.ts +++ b/packages/global/openapi/support/wallet/bill/api.ts @@ -5,9 +5,14 @@ import { BillStatusEnum, BillPayWayEnum } from '../../../../support/wallet/bill/constants'; -import { StandardSubLevelEnum, SubModeEnum } from '../../../../support/wallet/sub/constants'; +import { + StandardSubLevelEnum, + SubModeEnum, + SubTypeEnum +} from '../../../../support/wallet/sub/constants'; import { PaginationSchema } from '../../../api'; import { BillSchema } from '../../../../support/wallet/bill/type'; +import { CouponTypeEnum } from '../../../../support/wallet/sub/coupon/constants'; // Bill list export const BillListQuerySchema = PaginationSchema.safeExtend({ @@ -85,7 +90,23 @@ export type CheckPayResultResponseType = z.infer; diff --git a/packages/global/support/wallet/sub/coupon/constants.ts b/packages/global/support/wallet/sub/coupon/constants.ts index d5c90a0060..e3d2f768c5 100644 --- a/packages/global/support/wallet/sub/coupon/constants.ts +++ b/packages/global/support/wallet/sub/coupon/constants.ts @@ -1,3 +1,5 @@ +export const COUPON_PREFIX = 'coupon-'; + export enum CouponTypeEnum { bank = 'bank', activity = 'activity' diff --git a/packages/service/support/wallet/sub/utils.ts b/packages/service/support/wallet/sub/utils.ts index 319ecd3792..e1bafdec7f 100644 --- a/packages/service/support/wallet/sub/utils.ts +++ b/packages/service/support/wallet/sub/utils.ts @@ -61,20 +61,20 @@ export const getTeamStandPlan = async ({ teamId }: { teamId: string }) => { standardConstants: standardConstants ? { ...standardConstants, - maxTeamMember: standard?.maxTeamMember || standardConstants.maxTeamMember, - maxAppAmount: standard?.maxApp || standardConstants.maxAppAmount, - maxDatasetAmount: standard?.maxDataset || standardConstants.maxDatasetAmount, - requestsPerMinute: standard?.requestsPerMinute || standardConstants.requestsPerMinute, + maxTeamMember: standard?.maxTeamMember ?? standardConstants.maxTeamMember, + maxAppAmount: standard?.maxApp ?? standardConstants.maxAppAmount, + maxDatasetAmount: standard?.maxDataset ?? standardConstants.maxDatasetAmount, + requestsPerMinute: standard?.requestsPerMinute ?? standardConstants.requestsPerMinute, chatHistoryStoreDuration: - standard?.chatHistoryStoreDuration || standardConstants.chatHistoryStoreDuration, - maxDatasetSize: standard?.maxDatasetSize || standardConstants.maxDatasetSize, + standard?.chatHistoryStoreDuration ?? standardConstants.chatHistoryStoreDuration, + maxDatasetSize: standard?.maxDatasetSize ?? standardConstants.maxDatasetSize, websiteSyncPerDataset: - standard?.websiteSyncPerDataset || standardConstants.websiteSyncPerDataset, + standard?.websiteSyncPerDataset ?? standardConstants.websiteSyncPerDataset, appRegistrationCount: - standard?.appRegistrationCount || standardConstants.appRegistrationCount, + standard?.appRegistrationCount ?? standardConstants.appRegistrationCount, auditLogStoreDuration: - standard?.auditLogStoreDuration || standardConstants.auditLogStoreDuration, - ticketResponseTime: standard?.ticketResponseTime || standardConstants.ticketResponseTime + standard?.auditLogStoreDuration ?? standardConstants.auditLogStoreDuration, + ticketResponseTime: standard?.ticketResponseTime ?? standardConstants.ticketResponseTime } : undefined }; @@ -176,8 +176,8 @@ export const getTeamPlanStatus = async ({ const standardMaxDatasetSize = standardPlan?.currentSubLevel && standardPlans - ? standardPlans[standardPlan.currentSubLevel]?.maxDatasetSize || - standardPlan?.maxDatasetSize || + ? standardPlan?.maxDatasetSize || + standardPlans[standardPlan.currentSubLevel]?.maxDatasetSize || Infinity : Infinity; const totalDatasetSize = @@ -196,21 +196,21 @@ export const getTeamPlanStatus = async ({ standardConstants: standardConstants ? { ...standardConstants, - maxTeamMember: standardPlan?.maxTeamMember || standardConstants.maxTeamMember, - maxAppAmount: standardPlan?.maxApp || standardConstants.maxAppAmount, - maxDatasetAmount: standardPlan?.maxDataset || standardConstants.maxDatasetAmount, - requestsPerMinute: standardPlan?.requestsPerMinute || standardConstants.requestsPerMinute, + maxTeamMember: standardPlan?.maxTeamMember ?? standardConstants.maxTeamMember, + maxAppAmount: standardPlan?.maxApp ?? standardConstants.maxAppAmount, + maxDatasetAmount: standardPlan?.maxDataset ?? standardConstants.maxDatasetAmount, + requestsPerMinute: standardPlan?.requestsPerMinute ?? standardConstants.requestsPerMinute, chatHistoryStoreDuration: - standardPlan?.chatHistoryStoreDuration || standardConstants.chatHistoryStoreDuration, - maxDatasetSize: standardPlan?.maxDatasetSize || standardConstants.maxDatasetSize, + standardPlan?.chatHistoryStoreDuration ?? standardConstants.chatHistoryStoreDuration, + maxDatasetSize: standardPlan?.maxDatasetSize ?? standardConstants.maxDatasetSize, websiteSyncPerDataset: standardPlan?.websiteSyncPerDataset || standardConstants.websiteSyncPerDataset, appRegistrationCount: - standardPlan?.appRegistrationCount || standardConstants.appRegistrationCount, + standardPlan?.appRegistrationCount ?? standardConstants.appRegistrationCount, auditLogStoreDuration: - standardPlan?.auditLogStoreDuration || standardConstants.auditLogStoreDuration, + standardPlan?.auditLogStoreDuration ?? standardConstants.auditLogStoreDuration, ticketResponseTime: - standardPlan?.ticketResponseTime || standardConstants.ticketResponseTime + standardPlan?.ticketResponseTime ?? standardConstants.ticketResponseTime } : undefined, diff --git a/packages/web/i18n/en/account.json b/packages/web/i18n/en/account.json index cac044c84e..ec07e6f1bf 100644 --- a/packages/web/i18n/en/account.json +++ b/packages/web/i18n/en/account.json @@ -98,7 +98,20 @@ "subscription_period": "Subscription cycle", "subscription_package": "Subscription package", "subscription_mode_month": "Duration", - "month": "moon", + "month": "month", + "coupon_included_packages": "Coupon packages", + "day": "day", "extra_dataset_size": "Additional knowledge base capacity", - "extra_ai_points": "AI points calculation standard" + "extra_ai_points": "AI points calculation standard", + "max_team_member": "Max team members", + "max_app_amount": "Max app amount", + "max_dataset_amount": "Max dataset amount", + "requests_per_minute": "Requests per minute", + "max_dataset_size": "Max dataset size", + "chat_history_store_duration": "Chat history storage duration", + "website_sync_per_dataset": "Website sync per dataset", + "app_registration_count": "App registration count", + "audit_log_store_duration": "Audit log storage duration", + "ticket_response_time": "Ticket response time", + "custom_config_details": "Custom configuration details" } diff --git a/packages/web/i18n/zh-CN/account.json b/packages/web/i18n/zh-CN/account.json index 9281134509..6e6ff57bed 100644 --- a/packages/web/i18n/zh-CN/account.json +++ b/packages/web/i18n/zh-CN/account.json @@ -3,17 +3,32 @@ "active_model": "可用模型", "add_default_model": "添加预设模型", "api_key": "API 密钥", + "app_registration_count": "应用备案数", + "audit_log_store_duration": "团队操作日志记录时长", + "bill_detail": "账单详情", "bills_and_invoices": "账单与发票", "channel": "模型渠道", + "chat_history_store_duration": "对话记录保留时长", "config_model": "模型配置", "confirm_logout": "确认退出登录?", "create_channel": "新增渠道", "create_model": "新增模型", + "custom_config_details": "定制配置详情", "custom_model": "自定义模型", + "day": "天", "default_model": "预设模型", "default_model_config": "默认模型配置", + "extra_ai_points": "额外 AI 积分", + "extra_dataset_size": "额外知识库容量", + "generation_time": "生成时间", + "has_invoice": "是否已开票", + "hour": "小时", "language": "语言与时区", "logout": "登出", + "max_app_amount": "Agent 上限", + "max_dataset_amount": "知识库上限", + "max_dataset_size": "知识库索引上限", + "max_team_member": "团队成员上限", "model.active": "启用", "model.alias": "别名", "model.alias_tip": "模型在系统中展示的名字,方便用户理解", @@ -76,29 +91,27 @@ "model.voices": "声音角色", "model.voices_tip": "通过一个数组配置多个,例如:\n[\n {\n \"label\": \"Alloy\",\n \"value\": \"alloy\"\n },\n {\n \"label\": \"Echo\",\n \"value\": \"echo\"\n }\n]", "model_provider": "模型提供商", + "month": "月", + "no": "否", "notifications": "通知", + "order_number": "订单号", + "order_type": "订单类型", + "payment_method": "支付方式", "personal_information": "个人信息", "personalization": "个性化", "promotion_records": "促销记录", + "requests_per_minute": "QPM", "reset_default": "恢复默认配置", + "status": "状态", + "subscription_mode_month": "时长", + "subscription_package": "订阅套餐", + "subscription_period": "订阅周期", + "support_wallet_amount": "金额", "team": "团队管理", "third_party": "第三方账号", + "ticket_response_time": "工单支持响应时间", "usage_records": "使用记录", - "bill_detail": "账单详情", - "order_number": "订单号", - "generation_time": "生成时间", - "order_type": "订单类型", - "status": "状态", - "payment_method": "支付方式", - "support_wallet_amount": "金额", - "yuan": "{{amount}}元", - "has_invoice": "是否已开票", + "website_sync_per_dataset": "站点同步最大页数", "yes": "是", - "no": "否", - "subscription_period": "订阅周期", - "subscription_package": "订阅套餐", - "subscription_mode_month": "时长", - "month": "月", - "extra_dataset_size": "额外知识库容量", - "extra_ai_points": "额外 AI 积分" + "yuan": "{{amount}}元" } diff --git a/packages/web/i18n/zh-Hant/account.json b/packages/web/i18n/zh-Hant/account.json index cbf481c700..02c7c52047 100644 --- a/packages/web/i18n/zh-Hant/account.json +++ b/packages/web/i18n/zh-Hant/account.json @@ -3,17 +3,32 @@ "active_model": "可用模型", "add_default_model": "新增預設模型", "api_key": "API 金鑰", + "app_registration_count": "應用備案數", + "audit_log_store_duration": "團隊操作日誌記錄時長", + "bill_detail": "帳單詳細資訊", "bills_and_invoices": "帳單與發票", "channel": "模型管道", + "chat_history_store_duration": "對話記錄保留時長", "config_model": "模型設定", - "confirm_logout": "確認登出登入?", - "create_channel": "新增頻道", + "confirm_logout": "確認登出?", + "create_channel": "新增管道", "create_model": "新增模型", + "custom_config_details": "定制配置詳情", "custom_model": "自訂模型", + "day": "天", "default_model": "預設模型", "default_model_config": "預設模型設定", + "extra_ai_points": "額外 AI 積分", + "extra_dataset_size": "額外知識庫容量", + "generation_time": "生成時間", + "has_invoice": "是否已開票", + "hour": "小時", "language": "語言與時區", "logout": "登出", + "max_app_amount": "Agent 上限", + "max_dataset_amount": "知識庫上限", + "max_dataset_size": "知識庫索引上限", + "max_team_member": "團隊成員上限", "model.active": "啟用", "model.alias": "別名", "model.alias_tip": "模型在系統中展示的名字,方便使用者理解", @@ -47,9 +62,9 @@ "model.max_quote": "知識庫最大引用", "model.max_temperature": "最大溫度", "model.model_id": "模型 ID", - "model.model_id_tip": "模型的唯一標識,也就是實際請求到服務商 model 的值,需要與 OneAPI 頻道中的模型對應。", + "model.model_id_tip": "模型的唯一標識,也就是實際請求到服務商 model 的值,需要與 OneAPI 管道中的模型對應。", "model.normalization": "歸一化處理", - "model.normalization_tip": "如果 Embedding API 未對向量值進行歸一化,可以啟用該開關,系統會進行歸一化處理。\n\n未歸一化的 API,表現為向量檢索得分會大於 1。", + "model.normalization_tip": "如果 Embedding API 未對向量值進行歸一化,可以啟用該開關,系統會進行歸一化處理。\n未歸一化的 API,表現為向量檢索得分會大於 1。", "model.output_price": "模型輸出價格", "model.output_price_tip": "語言模型輸出價格,如果設定了該項,則模型綜合價格會失效", "model.param_name": "參數名稱", @@ -58,7 +73,7 @@ "model.request_auth": "自訂請求 Key", "model.request_auth_tip": "向自訂請求地址發起請求時候,攜帶請求頭:Authorization: Bearer xxx 進行請求", "model.request_url": "自訂請求地址", - "model.request_url_tip": "如果填寫該值,則會直接向該地址發起請求,不經過 OneAPI。\n需要遵循 OpenAI 的 API 格式,並填寫完整請求地址,例如:\n\nLLM: {{host}}/v1/chat/completions\n\nEmbedding: {{host}}/v1/embeddings\n\nSTT: {{host}}/v1/audio/transcriptions\n\nTTS: {{host}}/v1/audio/speech\n\nRerank: {{host}}/v1/rerank", + "model.request_url_tip": "如果填寫該值,則會直接向該地址發起請求,不經過 OneAPI。需要遵循 OpenAI 的 API 格式,並填寫完整請求地址,例如:\nLLM: {{host}}/v1/chat/completions\nEmbedding: {{host}}/v1/embeddings\nSTT: {{host}}/v1/audio/transcriptions\nTTS: {{host}}/v1/audio/speech\nRerank: {{host}}/v1/rerank", "model.response_format": "響應格式", "model.show_stop_sign": "展示停止序列參數", "model.show_top_p": "展示 Top-p 參數", @@ -74,31 +89,29 @@ "model.vision_tag": "視覺", "model.vision_tip": "如果模型支援圖片識別,則開啟該開關。", "model.voices": "聲音角色", - "model.voices_tip": "透過一個陣列設定多個,例如:\n\n[\n {\n \"label\": \"Alloy\",\n \"value\": \"alloy\"\n },\n {\n \"label\": \"Echo\",\n \"value\": \"echo\"\n }\n]", + "model.voices_tip": "透過一個陣列設定多個,例如:\n[\n {\n \"label\": \"Alloy\",\n \"value\": \"alloy\"\n },\n {\n \"label\": \"Echo\",\n \"value\": \"echo\"\n }\n]", "model_provider": "模型提供者", + "month": "月", + "no": "否", "notifications": "通知", + "order_number": "訂單編號", + "order_type": "訂單類型", + "payment_method": "支付方式", "personal_information": "個人資訊", "personalization": "個人化", "promotion_records": "促銷記錄", + "requests_per_minute": "QPM", "reset_default": "恢復預設設定", - "team": "團隊管理", - "third_party": "第三方賬號", - "usage_records": "使用記錄", - "bill_detail": "帳單詳細資訊", - "order_number": "訂單編號", - "generation_time": "生成時間", - "order_type": "訂單類型", "status": "狀態", - "payment_method": "支付方式", - "support_wallet_amount": "金額", - "yuan": "{{amount}}元", - "has_invoice": "是否已開票", - "yes": "是", - "no": "否", - "subscription_period": "訂閱週期", - "subscription_package": "訂閱套餐", "subscription_mode_month": "時長", - "month": "月", - "extra_dataset_size": "額外知識庫容量", - "extra_ai_points": "AI 積分運算標準" -} + "subscription_package": "訂閱套餐", + "subscription_period": "訂閱週期", + "support_wallet_amount": "金額", + "team": "團隊管理", + "third_party": "第三方帳號", + "ticket_response_time": "工單支援響應時間", + "usage_records": "使用記錄", + "website_sync_per_dataset": "站點同步最大頁數", + "yes": "是", + "yuan": "{{amount}}元" +} \ No newline at end of file diff --git a/projects/app/src/components/Layout/navbar.tsx b/projects/app/src/components/Layout/navbar.tsx index 00a65fd370..4da88ba8c3 100644 --- a/projects/app/src/components/Layout/navbar.tsx +++ b/projects/app/src/components/Layout/navbar.tsx @@ -123,7 +123,7 @@ const Navbar = ({ unread }: { unread: number }) => { w={'100%'} userSelect={'none'} pb={2} - bg={isDashboardPage ? 'white' : 'transparent'} + bg={isDashboardPage ? 'myGray.50' : 'transparent'} > {/* logo */} diff --git a/projects/app/src/components/support/wallet/StandardPlanContentList.tsx b/projects/app/src/components/support/wallet/StandardPlanContentList.tsx index 0bf5600a13..6a2d8599ef 100644 --- a/projects/app/src/components/support/wallet/StandardPlanContentList.tsx +++ b/projects/app/src/components/support/wallet/StandardPlanContentList.tsx @@ -35,18 +35,18 @@ const StandardPlanContentList = ({ level: level as `${StandardSubLevelEnum}`, ...standardSubLevelMap[level as `${StandardSubLevelEnum}`], totalPoints: - standplan?.totalPoints || plan.totalPoints * (mode === SubModeEnum.month ? 1 : 12), - requestsPerMinute: standplan?.requestsPerMinute || plan.requestsPerMinute || 2000, - maxTeamMember: standplan?.maxTeamMember || plan.maxTeamMember, - maxAppAmount: standplan?.maxApp || plan.maxAppAmount, - maxDatasetAmount: standplan?.maxDataset || plan.maxDatasetAmount, - maxDatasetSize: standplan?.maxDatasetSize || plan.maxDatasetSize, - websiteSyncPerDataset: standplan?.websiteSyncPerDataset || plan.websiteSyncPerDataset, + standplan?.totalPoints ?? plan.totalPoints * (mode === SubModeEnum.month ? 1 : 12), + requestsPerMinute: standplan?.requestsPerMinute ?? plan.requestsPerMinute, + maxTeamMember: standplan?.maxTeamMember ?? plan.maxTeamMember, + maxAppAmount: standplan?.maxApp ?? plan.maxAppAmount, + maxDatasetAmount: standplan?.maxDataset ?? plan.maxDatasetAmount, + maxDatasetSize: standplan?.maxDatasetSize ?? plan.maxDatasetSize, + websiteSyncPerDataset: standplan?.websiteSyncPerDataset ?? plan.websiteSyncPerDataset, chatHistoryStoreDuration: - standplan?.chatHistoryStoreDuration || plan.chatHistoryStoreDuration, - auditLogStoreDuration: standplan?.auditLogStoreDuration || plan.auditLogStoreDuration, - appRegistrationCount: standplan?.appRegistrationCount || plan.appRegistrationCount, - ticketResponseTime: standplan?.ticketResponseTime || plan.ticketResponseTime + standplan?.chatHistoryStoreDuration ?? plan.chatHistoryStoreDuration, + auditLogStoreDuration: standplan?.auditLogStoreDuration ?? plan.auditLogStoreDuration, + appRegistrationCount: standplan?.appRegistrationCount ?? plan.appRegistrationCount, + ticketResponseTime: standplan?.ticketResponseTime ?? plan.ticketResponseTime }; }, [ subPlans?.standard, diff --git a/projects/app/src/pageComponents/account/bill/BillDetailModal.tsx b/projects/app/src/pageComponents/account/bill/BillDetailModal.tsx index 484de813c3..5bc963ffd1 100644 --- a/projects/app/src/pageComponents/account/bill/BillDetailModal.tsx +++ b/projects/app/src/pageComponents/account/bill/BillDetailModal.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import MyModal from '@fastgpt/web/components/common/MyModal'; import { Box, Flex, ModalBody } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; @@ -13,6 +13,7 @@ import { formatStorePrice2Read } from '@fastgpt/global/support/wallet/usage/tool import { standardSubLevelMap, subModeMap } from '@fastgpt/global/support/wallet/sub/constants'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { getBillDetail } from '@/web/support/wallet/bill/api'; +import { i18nT } from '@fastgpt/web/i18n/utils'; type BillDetailModalProps = { billId: string; @@ -27,6 +28,91 @@ const BillDetailModal = ({ billId, onClose }: BillDetailModalProps) => { manual: false }); + const customConfigItems = useMemo(() => { + if (bill?.metadata.standSubLevel !== 'custom') return []; + const customSub = bill?.couponDetail?.subscriptions?.find( + (sub) => sub.type === 'standard' && sub.level === 'custom' && sub.customConfig + ); + + if (!customSub?.customConfig) return []; + + const config = customSub.customConfig; + const items = []; + + if (config.maxTeamMember !== undefined) { + items.push({ + key: i18nT('account:max_team_member'), + value: config.maxTeamMember, + unit: '' + }); + } + if (config.maxAppAmount !== undefined) { + items.push({ + key: i18nT('account:max_app_amount'), + value: config.maxAppAmount, + unit: '' + }); + } + if (config.maxDatasetAmount !== undefined) { + items.push({ + key: i18nT('account:max_dataset_amount'), + value: config.maxDatasetAmount, + unit: '' + }); + } + if (config.requestsPerMinute !== undefined) { + items.push({ + key: i18nT('account:requests_per_minute'), + value: config.requestsPerMinute, + unit: '' + }); + } + if (config.maxDatasetSize !== undefined) { + items.push({ + key: i18nT('account:max_dataset_size'), + value: config.maxDatasetSize, + unit: 'GB' + }); + } + if (config.chatHistoryStoreDuration !== undefined) { + items.push({ + key: i18nT('account:chat_history_store_duration'), + value: config.chatHistoryStoreDuration, + unit: 'day' + }); + } + if (config.websiteSyncPerDataset !== undefined) { + items.push({ + key: i18nT('account:website_sync_per_dataset'), + value: config.websiteSyncPerDataset, + unit: '' + }); + } + if (config.appRegistrationCount !== undefined) { + items.push({ + key: i18nT('account:app_registration_count'), + value: config.appRegistrationCount, + unit: '' + }); + } + if (config.auditLogStoreDuration !== undefined) { + items.push({ + key: i18nT('account:audit_log_store_duration'), + value: config.auditLogStoreDuration, + unit: 'day' + }); + } + if (config.ticketResponseTime !== undefined) { + items.push({ + key: i18nT('account:ticket_response_time'), + value: config.ticketResponseTime, + unit: 'h' + }); + } + + return items; + }, [bill?.couponDetail?.subscriptions]); + return ( { {t(billStatusMap[bill.status]?.label as any)} )} - {!!bill?.couponName && ( + {!!bill?.discountCouponName && ( {t('account_info:discount_coupon')}: - {t(bill?.couponName as any)} + {t(bill?.discountCouponName as any)} )} {!!bill?.metadata?.payWay && ( @@ -115,6 +201,24 @@ const BillDetailModal = ({ billId, onClose }: BillDetailModalProps) => { {bill.metadata.extraPoints} )} + {customConfigItems.length > 0 && ( + + {t('account:custom_config_details')}: + + {customConfigItems.map((item, idx) => ( + + {t(item.key)}: {item.value} + {item.unit && + (item.unit === 'day' + ? t('account:day') + : item.unit === 'h' + ? t('account:hour') + : item.unit)} + + ))} + + + )} );