diff --git a/.env-sample b/.env-sample index 604ff56f..009fda2f 100644 --- a/.env-sample +++ b/.env-sample @@ -82,6 +82,9 @@ NOSTR_SK='' # Number of currencies allowed in a community COMMUNITY_CURRENCIES=20 +# Max number of payment methods that can be configured in a community for wizzard selection +MAX_COMMUNITY_PAYMENT_METHODS=10 + # List of relays to connect to RELAYS='ws://localhost:7000,ws://localhost:8000,ws://localhost:9000' diff --git a/bot/middleware/stage.ts b/bot/middleware/stage.ts index 424eda3a..96bd2ebd 100644 --- a/bot/middleware/stage.ts +++ b/bot/middleware/stage.ts @@ -23,6 +23,7 @@ export const stageMiddleware = () => { CommunityModule.Scenes.updateFeeCommunityWizard, CommunityModule.Scenes.updateDisputeChannelCommunityWizard, CommunityModule.Scenes.updateLanguageCommunityWizard, + CommunityModule.Scenes.updatePaymentMethodsCommunityWizard, CommunityModule.Scenes.addEarningsInvoiceWizard, addInvoicePHIWizard, OrdersModule.Scenes.createOrder, diff --git a/bot/modules/community/commands.ts b/bot/modules/community/commands.ts index 1170cbd1..78e60525 100644 --- a/bot/modules/community/commands.ts +++ b/bot/modules/community/commands.ts @@ -206,6 +206,12 @@ export const updateCommunity = async ( user, community, }); + } else if (field === 'payment_methods') { + ctx.scene.enter('UPDATE_PAYMENT_METHODS_COMMUNITY_WIZARD_SCENE_ID', { + id, + user, + community, + }); } } catch (error) { logger.error(error); diff --git a/bot/modules/community/communityContext.ts b/bot/modules/community/communityContext.ts index f2c15303..ae8f213d 100644 --- a/bot/modules/community/communityContext.ts +++ b/bot/modules/community/communityContext.ts @@ -45,6 +45,7 @@ export interface CommunityWizardState { seller: UserDocument; type: string; method: string; + selectedMethods: string[]; bot: CommunityContext; message: Message.TextMessage | undefined; error?: any; diff --git a/bot/modules/community/index.ts b/bot/modules/community/index.ts index 9b9eacf9..e9ac6da4 100644 --- a/bot/modules/community/index.ts +++ b/bot/modules/community/index.ts @@ -57,6 +57,13 @@ export const configure = (bot: Telegraf) => { bot.action(/^editLanguageBtn_([0-9a-f]{24})$/, userMiddleware, async ctx => { await commands.updateCommunity(ctx, ctx.match[1], 'language'); }); + bot.action( + /^editPaymentMethodsBtn_([0-9a-f]{24})$/, + userMiddleware, + async ctx => { + await commands.updateCommunity(ctx, ctx.match[1], 'payment_methods'); + }, + ); bot.command('findcomms', userMiddleware, commands.findCommunity); bot.action( diff --git a/bot/modules/community/messages.ts b/bot/modules/community/messages.ts index 37be5e97..94b2c8d6 100644 --- a/bot/modules/community/messages.ts +++ b/bot/modules/community/messages.ts @@ -100,6 +100,12 @@ export const updateCommunityMessage = async (ctx: MainContext) => { callback_data: `editLanguageBtn_${id}`, }, ], + [ + { + text: '✏️ ' + ctx.i18n.t('community_payment_methods'), + callback_data: `editPaymentMethodsBtn_${id}`, + }, + ], [ { text: '💰 ' + ctx.i18n.t('earnings'), diff --git a/bot/modules/community/scenes.ts b/bot/modules/community/scenes.ts index 5551510e..79d9ed3c 100644 --- a/bot/modules/community/scenes.ts +++ b/bot/modules/community/scenes.ts @@ -916,6 +916,69 @@ export const updateLanguageCommunityWizard = new Scenes.WizardScene( }, ); +export const updatePaymentMethodsCommunityWizard = new Scenes.WizardScene( + 'UPDATE_PAYMENT_METHODS_COMMUNITY_WIZARD_SCENE_ID', + async (ctx: CommunityContext) => { + try { + const { community } = ctx.wizard.state; + const current = community.payment_methods?.join(', ') || ''; + let message = current + ? ctx.i18n.t('current_payment_methods', { methods: current }) + '\n\n' + : ''; + message += ctx.i18n.t('enter_community_payment_methods') + '\n\n'; + message += ctx.i18n.t('payment_methods_wizard_commands'); + await ctx.reply(message); + return ctx.wizard.next(); + } catch (error) { + logger.error(error); + ctx.scene.leave(); + } + }, + async (ctx: CommunityContext) => { + try { + if (ctx.message === undefined) return ctx.scene.leave(); + if (!('text' in ctx.message)) return; + + const text = ctx.message.text.trim(); + const methods = text + .split(',') + .map(m => m.trim()) + .filter(m => m.length > 0); + + const max = parseInt(process.env.MAX_COMMUNITY_PAYMENT_METHODS || '10'); + if (methods.length > max) { + return await ctx.reply(ctx.i18n.t('max_allowed', { max })); + } + + const { community } = ctx.wizard.state; + community.payment_methods = methods; + await community.save(); + await ctx.reply(ctx.i18n.t('payment_methods_saved')); + + return ctx.scene.leave(); + } catch (error) { + logger.error(error); + ctx.scene.leave(); + } + }, +); + +updatePaymentMethodsCommunityWizard.command( + 'reset', + async (ctx: CommunityContext) => { + try { + const { community } = ctx.wizard.state; + community.payment_methods = []; + await community.save(); + await ctx.reply(ctx.i18n.t('payment_methods_reset')); + return ctx.scene.leave(); + } catch (error) { + logger.error(error); + ctx.scene.leave(); + } + }, +); + export const addEarningsInvoiceWizard = new Scenes.WizardScene( 'ADD_EARNINGS_INVOICE_WIZARD_SCENE_ID', async (ctx: CommunityContext) => { diff --git a/bot/modules/orders/scenes.ts b/bot/modules/orders/scenes.ts index e7db533e..1eb6a497 100644 --- a/bot/modules/orders/scenes.ts +++ b/bot/modules/orders/scenes.ts @@ -1,6 +1,7 @@ import { Scenes, Markup } from 'telegraf'; import { logger } from '../../../logger'; import { getCurrency } from '../../../util'; +import { Community } from '../../../models'; import * as ordersActions from '../../ordersActions'; import { publishBuyOrderMessage, @@ -30,6 +31,7 @@ export const createOrder = new Scenes.WizardScene( sats, priceMargin, method, + selectedMethods, } = ctx.wizard.state; if (!statusMessage) { const { text } = messages.createOrderWizardStatus( @@ -64,8 +66,11 @@ export const createOrder = new Scenes.WizardScene( if (undefined === priceMargin && sats === 0) return createOrderSteps.priceMargin(ctx); if (undefined === method) return createOrderSteps.method(ctx); - // We remove all special characters from the payment method - const paymentMethod = method.replace(/[&/\\#,+~%.'":*?<>{}]/g, ''); + + const replaceRegex = /[&/\\#,+~%.'":*?<>{}]/g; + const paymentMethod = selectedMethods?.length + ? selectedMethods.map(m => m.replace(replaceRegex, '')).join(', ') + : method.replace(replaceRegex, ''); const order = await ordersActions.createOrder(ctx.i18n, ctx, user, { type, @@ -145,19 +150,143 @@ const createOrderSteps = { return ctx.wizard.next(); }, async method(ctx: CommunityContext) { - ctx.wizard.state.handler = async ctx => { - if (ctx.message === undefined) return ctx.scene.leave(); - const { text } = ctx.message; - if (!text) return; - ctx.wizard.state.method = text; - await ctx.wizard.state.updateUI(); - await ctx.deleteMessage(); - return await ctx.telegram.deleteMessage( - prompt.chat.id, - prompt.message_id, + const { user } = ctx.wizard.state; + const stateComm = ctx.wizard.state.community; + const loadedComm = + !stateComm && user?.default_community_id + ? await Community.findById(user.default_community_id) + : null; + const community = stateComm ?? loadedComm; + if (loadedComm) ctx.wizard.state.community = loadedComm; + const paymentMethods = community?.payment_methods; + + if (!paymentMethods || paymentMethods.length === 0) { + ctx.wizard.state.handler = async ctx => { + if (ctx.message === undefined) return ctx.scene.leave(); + if (!('text' in ctx.message)) return; + const { text } = ctx.message; + if (!text) return; + ctx.wizard.state.method = text; + await ctx.wizard.state.updateUI(); + await ctx.deleteMessage(); + return await ctx.telegram.deleteMessage( + prompt.chat.id, + prompt.message_id, + ); + }; + const prompt = await ctx.reply(ctx.i18n.t('enter_payment_method')); + return ctx.wizard.next(); + } + + ctx.wizard.state.selectedMethods = []; + const i18n = ctx.i18n; + + const buildKeyboard = (selected: string[]) => { + const buttons = paymentMethods.map((m, i) => + Markup.button.callback( + (selected.includes(m) ? '✓ ' : '') + m, + `pm_toggle_${i}`, + ), ); + const rows = []; + for (let i = 0; i < buttons.length; i += 2) { + rows.push(buttons.slice(i, i + 2)); + } + rows.push([ + Markup.button.callback(i18n.t('confirm_payment_methods'), 'pm_confirm'), + ]); + rows.push([ + Markup.button.callback(i18n.t('custom_payment_method'), 'pm_custom'), + ]); + return Markup.inlineKeyboard(rows); }; - const prompt = await ctx.reply(ctx.i18n.t('enter_payment_method')); + + const prompt = await ctx.reply( + ctx.i18n.t('select_payment_methods'), + buildKeyboard([]), + ); + + ctx.wizard.state.handler = async ctx => { + if (!ctx.callbackQuery) { + if (ctx.message === undefined || !('text' in ctx.message)) return; + const { text } = ctx.message; + if (!text) return; + ctx.wizard.state.selectedMethods = []; + ctx.wizard.state.method = text; + await ctx.wizard.state.updateUI(); + await ctx.deleteMessage(); + await ctx.telegram.deleteMessage(prompt.chat.id, prompt.message_id); + return true; + } + + const data = (ctx.callbackQuery as any).data as string; + + if (data === 'pm_confirm') { + const selected = ctx.wizard.state.selectedMethods || []; + if (selected.length === 0) { + await ctx.answerCbQuery(ctx.i18n.t('no_payment_method_selected')); + return; + } + ctx.wizard.state.method = selected.join(', '); + await ctx.wizard.state.updateUI(); + await ctx.telegram.deleteMessage(prompt.chat.id, prompt.message_id); + await ctx.answerCbQuery(); + return true; + } + + if (data === 'pm_custom') { + await ctx.telegram.deleteMessage(prompt.chat.id, prompt.message_id); + await ctx.answerCbQuery(); + const textPrompt = await ctx.reply(ctx.i18n.t('enter_payment_method')); + ctx.wizard.state.handler = async ctx => { + if (ctx.message === undefined || !('text' in ctx.message)) return; + const { text } = ctx.message; + if (!text) return; + ctx.wizard.state.selectedMethods = []; + ctx.wizard.state.method = text; + await ctx.wizard.state.updateUI(); + await ctx.deleteMessage(); + await ctx.telegram.deleteMessage( + textPrompt.chat.id, + textPrompt.message_id, + ); + return true; + }; + return; + } + + if (data.startsWith('pm_toggle_')) { + const methodIdx = parseInt(data.slice('pm_toggle_'.length), 10); + const m = paymentMethods[methodIdx]; + if (m === undefined) { + await ctx.answerCbQuery(); + return; + } + const selected = ctx.wizard.state.selectedMethods || []; + const idx = selected.indexOf(m); + if (idx >= 0) { + selected.splice(idx, 1); + } else { + selected.push(m); + } + ctx.wizard.state.selectedMethods = selected; + try { + await ctx.telegram.editMessageReplyMarkup( + prompt.chat.id, + prompt.message_id, + undefined, + buildKeyboard(selected).reply_markup, + ); + } catch (_err) { + // ignore transient errors (e.g. "message is not modified" on rapid taps) + } + await ctx.answerCbQuery(); + return; + } + + await ctx.answerCbQuery(); + }; + return ctx.wizard.next(); }, async priceMargin(ctx: CommunityContext) { diff --git a/locales/de.yaml b/locales/de.yaml index 8aa1b665..07e691ab 100644 --- a/locales/de.yaml +++ b/locales/de.yaml @@ -707,3 +707,13 @@ unblock_failed: "Fehler beim Freigeben des Benutzers" check_solvers: Ihre Community ${communityName} hat keine Solver. Bitte fügen Sie innerhalb von ${remainingDays} Tagen mindestens einen hinzu, um zu verhindern, dass die Community deaktiviert wird. check_solvers_last_warning: Ihre Community ${communityName} hat keine Solver. Bitte fügen Sie noch heute mindestens einen hinzu, um zu verhindern, dass die Community deaktiviert wird. image_processing_error: Wir hatten einen Fehler beim Verarbeiten des Bildes, bitte warten Sie ein paar Minuten und versuchen Sie es erneut. +community_payment_methods: "Zahlungsmethoden" +enter_community_payment_methods: "Gib die in deiner Community akzeptierten Zahlungsmethoden ein, getrennt durch Kommas (z.B.: Banküberweisung, Bargeld, PayPal)" +current_payment_methods: "Aktuelle Zahlungsmethoden: ${methods}" +select_payment_methods: "Wähle eine oder mehrere Zahlungsmethoden:" +confirm_payment_methods: "✅ Bestätigen" +no_payment_method_selected: "Bitte wähle mindestens eine Zahlungsmethode aus" +payment_methods_saved: "Zahlungsmethoden gespeichert ✅" +custom_payment_method: "✍️ Benutzerdefinierte Zahlungsmethode" +payment_methods_reset: "Zahlungsmethoden entfernt. Benutzer können jetzt beliebige Zahlungsmethoden frei eingeben." +payment_methods_wizard_commands: "/reset — alle Zahlungsmethoden entfernen und Standardverhalten wiederherstellen\n/exit — ohne Speichern beenden" diff --git a/locales/en.yaml b/locales/en.yaml index c12c9407..def6f4cd 100644 --- a/locales/en.yaml +++ b/locales/en.yaml @@ -712,3 +712,13 @@ unblock_failed: "Error unblocking the user" check_solvers: Your community ${communityName} does not have any solvers. Please add at least one within ${remainingDays} days to prevent the community from being disabled. check_solvers_last_warning: Your community ${communityName} does not have any solvers. Please add at least one today to prevent the community from being disabled. image_processing_error: We had an error processing the image, please wait a few minutes and try again +community_payment_methods: "Payment methods" +enter_community_payment_methods: "Enter the payment methods accepted in your community, separated by commas (e.g.: Bank transfer, Cash, PayPal)" +current_payment_methods: "Current payment methods: ${methods}" +select_payment_methods: "Select one or more payment methods:" +confirm_payment_methods: "✅ Confirm" +no_payment_method_selected: "Please select at least one payment method" +payment_methods_saved: "Payment methods saved ✅" +custom_payment_method: "✍️ Custom payment method" +payment_methods_reset: "Payment methods removed. Users can now enter any payment method freely." +payment_methods_wizard_commands: "/reset — remove all payment methods and restore default behavior\n/exit — exit without saving" diff --git a/locales/es.yaml b/locales/es.yaml index b77314c4..3825ad63 100644 --- a/locales/es.yaml +++ b/locales/es.yaml @@ -709,3 +709,13 @@ unblock_failed: "Error al desbloquear al usuario" image_processing_error: Hemos tenido un error procesando la imagen, por favor espera unos minutos y vuelve a intentarlo. check_solvers: Tu comunidad ${communityName} no tiene ningún solucionador. Agregue al menos uno dentro de ${remainingDays} días para evitar que se deshabilite la comunidad. check_solvers_last_warning: Tu comunidad ${communityName} no tiene ningún solucionador. Agregue al menos uno hoy para evitar que la comunidad quede inhabilitada. +community_payment_methods: "Métodos de pago" +enter_community_payment_methods: "Ingresa los métodos de pago aceptados en tu comunidad, separados por comas (ej.: Transferencia bancaria, Efectivo, PayPal)" +current_payment_methods: "Métodos de pago actuales: ${methods}" +select_payment_methods: "Selecciona uno o más métodos de pago:" +confirm_payment_methods: "✅ Confirmar" +no_payment_method_selected: "Por favor selecciona al menos un método de pago" +payment_methods_saved: "Métodos de pago guardados ✅" +custom_payment_method: "✍️ Método de pago personalizado" +payment_methods_reset: "Métodos de pago eliminados. Los usuarios ahora pueden ingresar cualquier método de pago libremente." +payment_methods_wizard_commands: "/reset — eliminar todos los métodos de pago y restaurar el comportamiento predeterminado\n/exit — salir sin guardar" diff --git a/locales/fa.yaml b/locales/fa.yaml index 824acd93..463206de 100644 --- a/locales/fa.yaml +++ b/locales/fa.yaml @@ -806,3 +806,13 @@ unblock_failed: "خطا در رفع مسدودیت کاربر" check_solvers: اجتماع شما ${communityName} هیچ داوری ندارد. لطفاً برای جلوگیری از غیرفعال شدن اجتماع، تا ${remainingDays} روز آینده حداقل یک داور به آن اضافه کنید. check_solvers_last_warning: اجتماع شما ${communityName} هیچ داوری ندارد. برای جلوگیری از غیرفعال شدن اجتماع، امروز حداقل یک داور به آن اضافه کنید. image_processing_error: هنگام پردازش تصویر با خطایی مواجه شدیم، لطفاً چند دقیقه صبر کرده و دوباره امتحان کنید. +community_payment_methods: "روش‌های پرداخت" +enter_community_payment_methods: "روش‌های پرداخت پذیرفته‌شده در اجتماع خود را با کاما از هم جدا کرده وارد کنید (مثال: انتقال بانکی، نقد، PayPal)" +current_payment_methods: "روش‌های پرداخت فعلی: ${methods}" +select_payment_methods: "یک یا چند روش پرداخت انتخاب کنید:" +confirm_payment_methods: "✅ تأیید" +no_payment_method_selected: "لطفاً حداقل یک روش پرداخت انتخاب کنید" +payment_methods_saved: "روش‌های پرداخت ذخیره شد ✅" +custom_payment_method: "✍️ روش پرداخت سفارشی" +payment_methods_reset: "روش‌های پرداخت حذف شدند. کاربران اکنون می‌توانند هر روش پرداختی را آزادانه وارد کنند." +payment_methods_wizard_commands: "/reset — حذف همه روش‌های پرداخت و بازگرداندن رفتار پیش‌فرض\n/exit — خروج بدون ذخیره" diff --git a/locales/fr.yaml b/locales/fr.yaml index 38a4fad0..e96d3e0b 100644 --- a/locales/fr.yaml +++ b/locales/fr.yaml @@ -706,3 +706,13 @@ unblock_failed: "Erreur lors du déblocage de l'utilisateur" check_solvers: Votre communauté ${communityName} ne possède aucun solveur. Veuillez en ajouter au moins un dans les ${remainingDays} jours pour éviter que la communauté ne soit désactivée. check_solvers_last_warning: Votre communauté ${communityName} ne possède aucun solveur. Veuillez en ajouter au moins un aujourd'hui pour éviter que la communauté ne soit désactivée. image_processing_error: Nous avons eu une erreur lors du traitement de l'image, veuillez attendre quelques minutes et réessayer. +community_payment_methods: "Méthodes de paiement" +enter_community_payment_methods: "Entrez les méthodes de paiement acceptées dans votre communauté, séparées par des virgules (ex. : Virement bancaire, Espèces, PayPal)" +current_payment_methods: "Méthodes de paiement actuelles : ${methods}" +select_payment_methods: "Sélectionnez une ou plusieurs méthodes de paiement :" +confirm_payment_methods: "✅ Confirmer" +no_payment_method_selected: "Veuillez sélectionner au moins une méthode de paiement" +payment_methods_saved: "Méthodes de paiement enregistrées ✅" +custom_payment_method: "✍️ Méthode de paiement personnalisée" +payment_methods_reset: "Méthodes de paiement supprimées. Les utilisateurs peuvent désormais saisir n'importe quelle méthode de paiement librement." +payment_methods_wizard_commands: "/reset — supprimer toutes les méthodes de paiement et restaurer le comportement par défaut\n/exit — quitter sans enregistrer" diff --git a/locales/it.yaml b/locales/it.yaml index 2a0efcd5..54aae511 100644 --- a/locales/it.yaml +++ b/locales/it.yaml @@ -704,3 +704,13 @@ unblock_failed: "Errore nello sbloccare l'utente" check_solvers: La tua community ${communityName} non ha risolutori. Aggiungine almeno uno entro ${remainingDays} giorni per evitare che la community venga disabilitata. check_solvers_last_warning: La tua community ${communityName} non ha risolutori. Per favore aggiungine almeno uno oggi per evitare che la community venga disabilitata. image_processing_error: Abbiamo avuto un errore nel processare l'immagine, per favore attendi qualche minuto e prova di nuovo. +community_payment_methods: "Metodi di pagamento" +enter_community_payment_methods: "Inserisci i metodi di pagamento accettati nella tua community, separati da virgole (es.: Bonifico bancario, Contanti, PayPal)" +current_payment_methods: "Metodi di pagamento attuali: ${methods}" +select_payment_methods: "Seleziona uno o più metodi di pagamento:" +confirm_payment_methods: "✅ Conferma" +no_payment_method_selected: "Seleziona almeno un metodo di pagamento" +payment_methods_saved: "Metodi di pagamento salvati ✅" +custom_payment_method: "✍️ Metodo di pagamento personalizzato" +payment_methods_reset: "Metodi di pagamento rimossi. Gli utenti possono ora inserire qualsiasi metodo di pagamento liberamente." +payment_methods_wizard_commands: "/reset — rimuovere tutti i metodi di pagamento e ripristinare il comportamento predefinito\n/exit — uscire senza salvare" diff --git a/locales/ko.yaml b/locales/ko.yaml index d2c15154..8880aa1d 100644 --- a/locales/ko.yaml +++ b/locales/ko.yaml @@ -704,3 +704,13 @@ unblock_failed: "사용자 차단 해제 중 오류 발생" check_solvers: ${communityName} 커뮤니티에 해결사가 없습니다. 커뮤니티가 비활성화되는 것을 방지하려면 ${remainingDays}일 이내에 하나 이상 추가하세요. check_solvers_last_warning: ${communityName} 커뮤니티에 해결사가 없습니다. 커뮤니티가 비활성화되는 것을 방지하려면 오늘 하나 이상 추가하세요. image_processing_error: 이미지 처리에 오류가 발생했습니다. 몇 분 후에 다시 시도해 주세요. +community_payment_methods: "결제 방법" +enter_community_payment_methods: "커뮤니티에서 허용되는 결제 방법을 쉼표로 구분하여 입력하세요 (예: 은행 이체, 현금, PayPal)" +current_payment_methods: "현재 결제 방법: ${methods}" +select_payment_methods: "하나 이상의 결제 방법을 선택하세요:" +confirm_payment_methods: "✅ 확인" +no_payment_method_selected: "결제 방법을 하나 이상 선택해 주세요" +payment_methods_saved: "결제 방법이 저장되었습니다 ✅" +custom_payment_method: "✍️ 사용자 지정 결제 방법" +payment_methods_reset: "결제 방법이 삭제되었습니다. 이제 사용자는 어떤 결제 방법이든 자유롭게 입력할 수 있습니다." +payment_methods_wizard_commands: "/reset — 모든 결제 방법을 삭제하고 기본 동작을 복원합니다\n/exit — 저장하지 않고 종료" diff --git a/locales/pt.yaml b/locales/pt.yaml index c4c9c1a8..7cfdebc2 100644 --- a/locales/pt.yaml +++ b/locales/pt.yaml @@ -706,3 +706,13 @@ unblock_failed: "Erro ao desbloquear o usuário" check_solvers: Sua comunidade ${communityName} não possui solucionadores. Adicione pelo menos um dentro de ${remainingDays} dias para evitar que a comunidade seja desativada. check_solvers_last_warning: Sua comunidade ${communityName} não possui solucionadores. Adicione pelo menos um hoje para evitar que a comunidade seja desativada. image_processing_error: Tivemos um erro ao processar a imagem, por favor aguarde alguns minutos e tente novamente. +community_payment_methods: "Métodos de pagamento" +enter_community_payment_methods: "Insira os métodos de pagamento aceitos em sua comunidade, separados por vírgulas (ex.: Transferência bancária, Dinheiro, PayPal)" +current_payment_methods: "Métodos de pagamento atuais: ${methods}" +select_payment_methods: "Selecione um ou mais métodos de pagamento:" +confirm_payment_methods: "✅ Confirmar" +no_payment_method_selected: "Por favor selecione pelo menos um método de pagamento" +payment_methods_saved: "Métodos de pagamento salvos ✅" +custom_payment_method: "✍️ Método de pagamento personalizado" +payment_methods_reset: "Métodos de pagamento removidos. Os usuários agora podem inserir qualquer método de pagamento livremente." +payment_methods_wizard_commands: "/reset — remover todos os métodos de pagamento e restaurar o comportamento padrão\n/exit — sair sem salvar" diff --git a/locales/ru.yaml b/locales/ru.yaml index 9404f3d8..09956b27 100644 --- a/locales/ru.yaml +++ b/locales/ru.yaml @@ -707,3 +707,13 @@ unblock_failed: "Ошибка при разблокировке пользова check_solvers: В вашем сообществе ${communityName} нет решателей. Добавьте хотя бы одно в течение ${remainingDays} дн., чтобы сообщество не было отключено. check_solvers_last_warning: В вашем сообществе ${communityName} нет решателей. Пожалуйста, добавьте хотя бы один сегодня, чтобы предотвратить отключение сообщества. image_processing_error: У нас возникла ошибка при обработке изображения, пожалуйста, подождите несколько минут и попробуйте снова. +community_payment_methods: "Способы оплаты" +enter_community_payment_methods: "Введите способы оплаты, принятые в вашем сообществе, разделённые запятыми (напр.: Банковский перевод, Наличные, PayPal)" +current_payment_methods: "Текущие способы оплаты: ${methods}" +select_payment_methods: "Выберите один или несколько способов оплаты:" +confirm_payment_methods: "✅ Подтвердить" +no_payment_method_selected: "Пожалуйста, выберите хотя бы один способ оплаты" +payment_methods_saved: "Способы оплаты сохранены ✅" +custom_payment_method: "✍️ Пользовательский способ оплаты" +payment_methods_reset: "Способы оплаты удалены. Пользователи теперь могут свободно вводить любой способ оплаты." +payment_methods_wizard_commands: "/reset — удалить все способы оплаты и восстановить поведение по умолчанию\n/exit — выйти без сохранения" diff --git a/locales/uk.yaml b/locales/uk.yaml index c3fcccc8..1176d9e4 100644 --- a/locales/uk.yaml +++ b/locales/uk.yaml @@ -703,3 +703,13 @@ unblock_failed: "Помилка при розблокуванні користу check_solvers: У вашій спільноті ${communityName} немає розв’язувачів. Додайте принаймні одну протягом ${remainingDays} днів, щоб запобігти вимкненню спільноти. check_solvers_last_warning: У вашій спільноті ${communityName} немає розв’язувачів. Будь ласка, додайте принаймні одну сьогодні, щоб запобігти вимкненню спільноти. image_processing_error: У нас виникла помилка при обробці зображення, будь ласка, почекайте кілька хвилин і спробуйте знову. +community_payment_methods: "Способи оплати" +enter_community_payment_methods: "Введіть способи оплати, прийняті у вашій спільноті, розділені комами (напр.: Банківський переказ, Готівка, PayPal)" +current_payment_methods: "Поточні способи оплати: ${methods}" +select_payment_methods: "Оберіть один або кілька способів оплати:" +confirm_payment_methods: "✅ Підтвердити" +no_payment_method_selected: "Будь ласка, оберіть принаймні один спосіб оплати" +payment_methods_saved: "Способи оплати збережено ✅" +custom_payment_method: "✍️ Власний спосіб оплати" +payment_methods_reset: "Способи оплати видалено. Користувачі тепер можуть вільно вводити будь-який спосіб оплати." +payment_methods_wizard_commands: "/reset — видалити всі способи оплати та відновити поведінку за замовчуванням\n/exit — вийти без збереження" diff --git a/models/community.ts b/models/community.ts index 8b1b8bb2..0434174e 100644 --- a/models/community.ts +++ b/models/community.ts @@ -48,6 +48,7 @@ export interface ICommunity extends Document { banned_users: Types.DocumentArray; public: boolean; currencies: Array; + payment_methods: Array; created_at: Date; nostr_public_key: string; warning_messages_count: number; @@ -83,6 +84,10 @@ const CommunitySchema = new Schema({ trim: true, validate: [currencyLimits, '{PATH} is not within limits'], }, + payment_methods: { + type: [String], + default: [], + }, created_at: { type: Date, default: Date.now }, nostr_public_key: { type: String }, warning_messages_count: { type: Number, default: 0 }, diff --git a/tests/bot/modules/orders/scenes.spec.ts b/tests/bot/modules/orders/scenes.spec.ts new file mode 100644 index 00000000..89c11010 --- /dev/null +++ b/tests/bot/modules/orders/scenes.spec.ts @@ -0,0 +1,237 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); +const proxyquire = require('proxyquire'); + +// Mock models +const CommunityMock = { + findById: sinon.stub(), +}; + +// Load scenes with mocked Community +const { createOrder } = proxyquire('../../../../bot/modules/orders/scenes', { + '../../../models': { + Community: CommunityMock, + }, +}); + +describe('Order Creation Wizard - Payment Methods', () => { + let ctx: any; + let sandbox: any; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + ctx = { + i18n: { + t: sandbox.stub().callsFake((key: string) => key), + }, + wizard: { + state: { + user: { tg_id: '123' }, + community: null, + updateUI: sandbox.stub().resolves(), + // To reach 'method' step in step 0: + currency: 'USD', + fiatAmount: [100], + sats: 1000, + priceMargin: 0, + method: undefined, + }, + next: sandbox.stub(), + selectStep: sandbox.stub(), + cursor: 0, + }, + scene: { + leave: sandbox.stub().resolves(), + }, + reply: sandbox.stub().resolves({ chat: { id: 1 }, message_id: 111 }), + telegram: { + deleteMessage: sandbox.stub().resolves(), + editMessageReplyMarkup: sandbox.stub().resolves(), + editMessageText: sandbox.stub().resolves(), + }, + answerCbQuery: sandbox.stub().resolves(), + deleteMessage: sandbox.stub().resolves(), + }; + CommunityMock.findById.reset(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + const runStep0 = async (ctx: any) => { + await createOrder.steps[0](ctx); + }; + + it('should prompt for custom text if community has no payment methods', async () => { + // Community with no payment methods + const community = { + id: 'comm123', + payment_methods: [], + }; + ctx.wizard.state.community = community; + + await runStep0(ctx); + + expect(ctx.reply.calledWith('enter_payment_method')).to.equal(true); + expect(ctx.wizard.next.calledOnce).to.equal(true); + expect(ctx.wizard.state.handler).to.be.a('function'); + + // Test the handler + const handler = ctx.wizard.state.handler; + const testCtx = { + message: { text: 'Custom Method' }, + deleteMessage: sandbox.stub().resolves(), + wizard: ctx.wizard, + telegram: ctx.telegram, + }; + + await handler(testCtx); + expect(ctx.wizard.state.method).to.equal('Custom Method'); + }); + + it('should show keyboard if community has payment methods', async () => { + const community = { + id: 'comm123', + payment_methods: ['Zelle', 'Bank Transfer'], + }; + ctx.wizard.state.community = community; + + await runStep0(ctx); + + expect(ctx.reply.calledWith('select_payment_methods')).to.equal(true); + expect(ctx.wizard.next.calledOnce).to.equal(true); + expect(ctx.wizard.state.selectedMethods).to.deep.equal([]); + }); + + it('should handle multi-select and confirmation', async () => { + const community = { + id: 'comm123', + payment_methods: ['Zelle', 'Bank Transfer'], + }; + ctx.wizard.state.community = community; + + await runStep0(ctx); + const handler = ctx.wizard.state.handler; + + // Simulate toggling Zelle + await handler({ + callbackQuery: { data: 'pm_toggle_0' }, + answerCbQuery: ctx.answerCbQuery, + telegram: ctx.telegram, + wizard: ctx.wizard, + }); + expect(ctx.wizard.state.selectedMethods).to.deep.equal(['Zelle']); + + // Simulate toggling Bank Transfer + await handler({ + callbackQuery: { data: 'pm_toggle_1' }, + answerCbQuery: ctx.answerCbQuery, + telegram: ctx.telegram, + wizard: ctx.wizard, + }); + expect(ctx.wizard.state.selectedMethods).to.deep.equal([ + 'Zelle', + 'Bank Transfer', + ]); + + // Confirm + await handler({ + callbackQuery: { data: 'pm_confirm' }, + answerCbQuery: ctx.answerCbQuery, + telegram: ctx.telegram, + wizard: ctx.wizard, + i18n: ctx.i18n, + }); + expect(ctx.wizard.state.method).to.equal('Zelle, Bank Transfer'); + }); + + it('should show error if confirmation with no selection', async () => { + const community = { + id: 'comm123', + payment_methods: ['Zelle', 'Bank Transfer'], + }; + ctx.wizard.state.community = community; + + await runStep0(ctx); + const handler = ctx.wizard.state.handler; + + await handler({ + callbackQuery: { data: 'pm_confirm' }, + answerCbQuery: ctx.answerCbQuery, + telegram: ctx.telegram, + wizard: ctx.wizard, + i18n: ctx.i18n, + }); + + expect(ctx.answerCbQuery.calledWith('no_payment_method_selected')).to.equal( + true, + ); + expect(ctx.wizard.state.method).to.equal(undefined); + }); + + it('should handle custom free-text fallback and clear selectedMethods', async () => { + const community = { + id: 'comm123', + payment_methods: ['Zelle', 'Bank Transfer'], + }; + ctx.wizard.state.community = community; + + await runStep0(ctx); + const handler = ctx.wizard.state.handler; + + // First select something + ctx.wizard.state.selectedMethods = ['Zelle']; + + // Then click custom + await handler({ + callbackQuery: { data: 'pm_custom' }, + answerCbQuery: ctx.answerCbQuery, + telegram: ctx.telegram, + wizard: ctx.wizard, + i18n: ctx.i18n, + reply: ctx.reply, + }); + + expect(ctx.reply.calledWith('enter_payment_method')).to.equal(true); + const customHandler = ctx.wizard.state.handler; + expect(customHandler).to.not.equal(handler); + + // Enter custom text + await customHandler({ + message: { text: 'Venmo' }, + deleteMessage: sandbox.stub().resolves(), + wizard: ctx.wizard, + telegram: ctx.telegram, + }); + + expect(ctx.wizard.state.method).to.equal('Venmo'); + expect(ctx.wizard.state.selectedMethods).to.deep.equal([]); + }); + + it('should clear selectedMethods on direct free-text input (fallback)', async () => { + const community = { + id: 'comm123', + payment_methods: ['Zelle', 'Bank Transfer'], + }; + ctx.wizard.state.community = community; + + await runStep0(ctx); + const handler = ctx.wizard.state.handler; + + // First select something + ctx.wizard.state.selectedMethods = ['Zelle']; + + // Send direct text message (instead of callback) + await handler({ + message: { text: 'Direct Text' }, + deleteMessage: sandbox.stub().resolves(), + wizard: ctx.wizard, + telegram: ctx.telegram, + }); + + expect(ctx.wizard.state.method).to.equal('Direct Text'); + expect(ctx.wizard.state.selectedMethods).to.deep.equal([]); + }); +}); +export {};