diff --git a/.env-sample b/.env-sample index 225c9707..31f10540 100644 --- a/.env-sample +++ b/.env-sample @@ -119,3 +119,21 @@ MONITOR_URL='' MONITOR_AUTH_TOKEN='' # Bot name identifier sent with heartbeats (default: lnp2pBot) MONITOR_BOT_NAME='lnp2pBot' + +# Scheduled orders: number of publication cycles per schedule (default: 10) +REPUBLISH_ORDER_DAYS='10' +# Anti-spam requirements to create scheduled orders (all optional, defaults shown) +# Max active schedules a single user can hold at once +SCHEDULE_MAX_PER_USER='3' +# Minimum account age (days using the bot) required to schedule orders +SCHEDULE_MIN_ACCOUNT_AGE_DAYS='7' +# Minimum number of completed trades required to schedule orders +SCHEDULE_MIN_COMPLETED_ORDERS='5' +# Minimum traded volume in sats required to schedule orders (0 disables the check) +SCHEDULE_MIN_VOLUME='0' +# Minimum reputation (0-5) required to schedule orders (enforced only if the user has reviews) +SCHEDULE_MIN_RATING='4' +# Minimum order completion rate (SUCCESS over taken orders), 0-1 +SCHEDULE_MIN_COMPLETION_RATE='0.9' +# Max consecutive uncompleted taken orders before a maker's schedules are removed as dormant (0 disables) +SCHEDULE_MAX_CONSECUTIVE_UNCOMPLETED='5' diff --git a/bot/middleware/stage.ts b/bot/middleware/stage.ts index 96bd2ebd..d7b90d8a 100644 --- a/bot/middleware/stage.ts +++ b/bot/middleware/stage.ts @@ -2,6 +2,7 @@ import { Scenes } from 'telegraf'; import * as CommunityModule from '../modules/community'; import * as OrdersModule from '../modules/orders'; import * as UserModule from '../modules/user'; +import { scheduleOrderWizard } from '../modules/schedule'; import { CommunityContext } from '../modules/community/communityContext'; import { addInvoiceWizard, @@ -28,6 +29,7 @@ export const stageMiddleware = () => { addInvoicePHIWizard, OrdersModule.Scenes.createOrder, UserModule.Scenes.Settings, + scheduleOrderWizard, ]; scenes.forEach(addGenericCommands); const stage = new Scenes.Stage(scenes, { diff --git a/bot/modules/orders/takeOrder.ts b/bot/modules/orders/takeOrder.ts index d705deba..e861fd5d 100644 --- a/bot/modules/orders/takeOrder.ts +++ b/bot/modules/orders/takeOrder.ts @@ -16,7 +16,6 @@ import { validateTakeSellOrder, validateUserWaitingOrder, } from '../../validations'; - const OrderEvents = require('../../modules/events/orders'); export const takeOrderActionValidation = async ( @@ -93,7 +92,6 @@ export const takebuy = async ( OrderEvents.orderUpdated(order); await deleteOrderFromChannel(order, bot.telegram); - await messages.beginTakeBuyMessage(ctx, bot, user, order); }); } catch (error) { diff --git a/bot/modules/schedule/commands.ts b/bot/modules/schedule/commands.ts new file mode 100644 index 00000000..41a42100 --- /dev/null +++ b/bot/modules/schedule/commands.ts @@ -0,0 +1,149 @@ +import { Types } from 'mongoose'; +import { CommunityContext } from '../community/communityContext'; +import { ScheduledOrder } from '../../../models'; +import { validateSellOrder, validateBuyOrder } from '../../validations'; +import { getCurrency } from '../../../util'; +import { logger } from '../../../logger'; +import { SCHEDULE_ORDER } from './scenes'; +import { formatDays } from './messages'; +import { checkScheduleRequirements } from './helpers'; + +export const scheduleorder = async (ctx: CommunityContext) => { + try { + // The scheduling flow is an interactive DM wizard + if (ctx.message?.chat.type !== 'private') return; + + const text = (ctx.message as any)?.text || ''; + const [, type, ...rest] = text.trim().split(/\s+/); + + if (!type || !['buy', 'sell'].includes(type.toLowerCase())) { + return ctx.reply(ctx.i18n.t('scheduleorder_usage')); + } + + const orderType = type.toLowerCase(); + + // Reuse the existing buy/sell validators by feeding them the order args + if (!ctx.state) (ctx as any).state = {}; + if (!ctx.state.command) (ctx as any).state.command = {}; + ctx.state.command.args = rest; + + const params = + orderType === 'sell' + ? await validateSellOrder(ctx) + : await validateBuyOrder(ctx); + + if (!params) return; + + if (!getCurrency(params.fiatCode)) { + await ctx.reply(ctx.i18n.t('must_be_valid_currency')); + return; + } + + // Gate creation behind anti-spam requirements (limit, seniority, completion + // rate, etc.) so only trusted makers can auto-publish orders. + const requirement = await checkScheduleRequirements(ctx.user); + if (!requirement.ok) { + await ctx.reply(ctx.i18n.t(requirement.messageKey!, requirement.params)); + return; + } + + await ctx.scene.enter(SCHEDULE_ORDER, { + user: ctx.user, + scheduleType: orderType, + amount: params.amount, + fiatAmount: params.fiatAmount, + fiatCode: params.fiatCode, + paymentMethod: params.paymentMethod, + priceMargin: params.priceMargin, + }); + } catch (error) { + logger.error(error); + } +}; + +export const cancelschedule = async (ctx: CommunityContext) => { + try { + const user = ctx.user; + const args = ctx.state?.command?.args || []; + const scheduleId = args[0]; + + if (!scheduleId || !Types.ObjectId.isValid(scheduleId)) { + return ctx.reply(ctx.i18n.t('cancelschedule_usage')); + } + + const schedule = await ScheduledOrder.findOne({ + _id: scheduleId, + creator_id: user._id, + active: true, + }); + + if (!schedule) { + return ctx.reply(ctx.i18n.t('schedule_not_found')); + } + + schedule.active = false; + await schedule.save(); + + await ctx.reply(ctx.i18n.t('schedule_cancelled', { scheduleId })); + } catch (error) { + logger.error(error); + } +}; + +export const listschedules = async (ctx: CommunityContext) => { + try { + const user = ctx.user; + + const schedules = await ScheduledOrder.find({ + creator_id: user._id, + active: true, + }).sort({ created_at: 1 }); + + if (schedules.length === 0) { + return ctx.reply(ctx.i18n.t('listschedules_empty')); + } + + const items = schedules + .map(schedule => { + const hour = String(schedule.hour).padStart(2, '0'); + const typeKey = schedule.type === 'buy' ? 'buying' : 'selling'; + return ctx.i18n.t('schedule_list_item', { + scheduleId: String(schedule._id), + type: ctx.i18n.t(typeKey), + fiatAmount: schedule.fiat_amount.join('-'), + fiatCode: schedule.fiat_code, + paymentMethod: schedule.payment_method, + days: formatDays(schedule.days), + hour: `${hour}:00 UTC`, + }); + }) + .join('\n'); + + await ctx.reply(`${ctx.i18n.t('listschedules_header')}\n\n${items}`, { + parse_mode: 'Markdown', + }); + } catch (error) { + logger.error(error); + } +}; + +export const cancelallschedules = async (ctx: CommunityContext) => { + try { + const user = ctx.user; + + const result = await ScheduledOrder.updateMany( + { creator_id: user._id, active: true }, + { active: false }, + ); + + if (result.modifiedCount === 0) { + return ctx.reply(ctx.i18n.t('cancelallschedules_none')); + } + + await ctx.reply( + ctx.i18n.t('all_schedules_cancelled', { count: result.modifiedCount }), + ); + } catch (error) { + logger.error(error); + } +}; diff --git a/bot/modules/schedule/helpers.ts b/bot/modules/schedule/helpers.ts new file mode 100644 index 00000000..f7372e6a --- /dev/null +++ b/bot/modules/schedule/helpers.ts @@ -0,0 +1,189 @@ +import { Order, ScheduledOrder } from '../../../models'; +import { UserDocument } from '../../../models/user'; + +const REPUBLISH_DAYS_DEFAULT = 10; + +export const getRepublishCount = (): number => { + const raw = parseInt(process.env.REPUBLISH_ORDER_DAYS || ''); + return Number.isInteger(raw) && raw > 0 ? raw : REPUBLISH_DAYS_DEFAULT; +}; + +// English day names (full and abbreviated) mapped to getUTCDay() values (0=Sun..6=Sat) +const DAY_ALIASES: Record = { + sunday: 0, + sun: 0, + monday: 1, + mon: 1, + tuesday: 2, + tue: 2, + wednesday: 3, + wed: 3, + thursday: 4, + thu: 4, + friday: 5, + fri: 5, + saturday: 6, + sat: 6, +}; + +// Parses a comma/space separated list of English day names into sorted UTC +// weekday values (0-6). Returns null if any token is unrecognized. +export const parseCustomDays = (input: string): number[] | null => { + const parts = input + .toLowerCase() + .split(/[,\s]+/) + .filter(Boolean); + if (parts.length === 0) return null; + const days = new Set(); + for (const part of parts) { + const dayNum = DAY_ALIASES[part]; + // Guard against inherited Object.prototype keys (e.g. "constructor", + // "toString") which would resolve to a function instead of undefined. + if (typeof dayNum !== 'number') return null; + days.add(dayNum); + } + return [...days].sort((a, b) => a - b); +}; + +export const PRESET_DAYS: Record = { + daily: [0, 1, 2, 3, 4, 5, 6], + weekdays: [1, 2, 3, 4, 5], + weekend: [0, 6], +}; + +const envNumber = (raw: string | undefined, fallback: number): number => { + const value = Number(raw); + return Number.isFinite(value) ? value : fallback; +}; + +export interface ScheduleRequirementResult { + ok: boolean; + messageKey?: string; + params?: Record; +} + +// Gates the creation of scheduled orders to prevent spam and UX degradation: +// only trusted, active makers with a good completion rate should be allowed to +// publish orders automatically. All thresholds are configurable via env. +export const checkScheduleRequirements = async ( + user: UserDocument, +): Promise => { + const maxPerUser = envNumber(process.env.SCHEDULE_MAX_PER_USER, 3); + const minAgeDays = envNumber(process.env.SCHEDULE_MIN_ACCOUNT_AGE_DAYS, 7); + const minCompleted = envNumber(process.env.SCHEDULE_MIN_COMPLETED_ORDERS, 5); + const minVolume = envNumber(process.env.SCHEDULE_MIN_VOLUME, 0); + const minRating = envNumber(process.env.SCHEDULE_MIN_RATING, 4); + const minCompletionRate = envNumber( + process.env.SCHEDULE_MIN_COMPLETION_RATE, + 0.9, + ); + + // Hard limit on how many active schedules a user can hold at once. + const activeCount = await ScheduledOrder.countDocuments({ + creator_id: user._id, + active: true, + }); + if (activeCount >= maxPerUser) { + return { + ok: false, + messageKey: 'schedule_req_max_reached', + params: { max: maxPerUser }, + }; + } + + // Minimal account age (days using the bot). + const ageDays = + (Date.now() - new Date(user.created_at).getTime()) / (1000 * 60 * 60 * 24); + if (ageDays < minAgeDays) { + return { + ok: false, + messageKey: 'schedule_req_account_age', + params: { days: minAgeDays }, + }; + } + + // Minimum number of completed trades. + if (user.trades_completed < minCompleted) { + return { + ok: false, + messageKey: 'schedule_req_completed_orders', + params: { count: minCompleted }, + }; + } + + // Minimum traded volume (skipped when threshold is 0). + if (minVolume > 0 && user.volume_traded < minVolume) { + return { + ok: false, + messageKey: 'schedule_req_volume', + params: { volume: minVolume }, + }; + } + + // Minimum reputation, only enforced once the user has received reviews. + if ( + minRating > 0 && + user.total_reviews > 0 && + user.total_rating < minRating + ) { + return { + ok: false, + messageKey: 'schedule_req_rating', + params: { rating: minRating }, + }; + } + + // Completion rate: successfully finished orders over the orders that were + // actually taken (taken_at set). Orders that expired in PENDING without a + // taker are excluded (they never wasted anyone's time), so this measures how + // often the maker follows through once a counterparty commits — which is what + // matters for auto-published orders. + const takenOrders = await Order.countDocuments({ + creator_id: user._id, + taken_at: { $ne: null }, + }); + if (takenOrders > 0) { + const successOrders = await Order.countDocuments({ + creator_id: user._id, + status: 'SUCCESS', + }); + const rate = successOrders / takenOrders; + if (rate < minCompletionRate) { + return { + ok: false, + messageKey: 'schedule_req_completion_rate', + params: { + rate: Math.round(minCompletionRate * 100), + current: Math.round(rate * 100), + }, + }; + } + } + + return { ok: true }; +}; + +// Hard limit on how many of a maker's most recent taken orders may go +// uncompleted in a row before their schedules are removed as dormant. +export const getDormantLimit = (): number => + envNumber(process.env.SCHEDULE_MAX_CONSECUTIVE_UNCOMPLETED, 5); + +// A maker is "dormant" when their last N taken orders all ended without +// success. Such a user keeps getting orders taken but never follows through, so +// their auto-published orders only waste takers' time. Returns false while +// there is not enough taken-order history to judge. +export const isDormantMaker = async (userId: string): Promise => { + const limit = getDormantLimit(); + if (limit <= 0) return false; + + const recent = await Order.find({ + creator_id: userId, + taken_at: { $ne: null }, + }) + .sort({ taken_at: -1 }) + .limit(limit) + .select('status'); + + if (recent.length < limit) return false; + return recent.every(order => order.status !== 'SUCCESS'); +}; diff --git a/bot/modules/schedule/index.ts b/bot/modules/schedule/index.ts new file mode 100644 index 00000000..4f11d731 --- /dev/null +++ b/bot/modules/schedule/index.ts @@ -0,0 +1,18 @@ +import { Telegraf } from 'telegraf'; +import { userMiddleware } from '../../middleware/user'; +import { CommunityContext } from '../community/communityContext'; +import { + scheduleorder, + cancelschedule, + listschedules, + cancelallschedules, +} from './commands'; + +export { scheduleOrderWizard } from './scenes'; + +export const configure = (bot: Telegraf) => { + bot.command('scheduleorder', userMiddleware, scheduleorder); + bot.command('cancelschedule', userMiddleware, cancelschedule); + bot.command('listschedules', userMiddleware, listschedules); + bot.command('cancelallschedules', userMiddleware, cancelallschedules); +}; diff --git a/bot/modules/schedule/messages.ts b/bot/modules/schedule/messages.ts new file mode 100644 index 00000000..51ef427c --- /dev/null +++ b/bot/modules/schedule/messages.ts @@ -0,0 +1,89 @@ +import { CommunityContext } from '../community/communityContext'; + +const DAY_LABELS: Record = { + 0: 'Sun', + 1: 'Mon', + 2: 'Tue', + 3: 'Wed', + 4: 'Thu', + 5: 'Fri', + 6: 'Sat', +}; + +export const formatDays = (days: number[]): string => + days.map(d => DAY_LABELS[d]).join(', '); + +export const askScheduleDays = async (ctx: CommunityContext) => { + await ctx.reply(ctx.i18n.t('schedule_choose_days'), { + reply_markup: { + inline_keyboard: [ + [ + { + text: '📅 ' + ctx.i18n.t('sched_daily'), + callback_data: 'sched_daily', + }, + { + text: '💼 ' + ctx.i18n.t('sched_weekdays'), + callback_data: 'sched_weekdays', + }, + ], + [ + { + text: '🌴 ' + ctx.i18n.t('sched_weekend'), + callback_data: 'sched_weekend', + }, + { + text: '⚙️ ' + ctx.i18n.t('sched_custom'), + callback_data: 'sched_custom', + }, + ], + ], + }, + }); +}; + +export const askCustomDays = async (ctx: CommunityContext) => { + await ctx.reply(ctx.i18n.t('schedule_enter_custom_days')); +}; + +export const askScheduleHour = async (ctx: CommunityContext) => { + await ctx.reply(ctx.i18n.t('schedule_enter_hour')); +}; + +export const askScheduleConfirm = async ( + ctx: CommunityContext, + summaryData: { + type: string; + fiatAmount: number[]; + fiatCode: string; + paymentMethod: string; + days: number[]; + hour: number; + }, +) => { + const hour = String(summaryData.hour).padStart(2, '0'); + const typeKey = summaryData.type === 'buy' ? 'buying' : 'selling'; + const summary = ctx.i18n.t('schedule_confirm_summary', { + type: ctx.i18n.t(typeKey), + fiatAmount: summaryData.fiatAmount.join('-'), + fiatCode: summaryData.fiatCode, + paymentMethod: summaryData.paymentMethod, + days: formatDays(summaryData.days), + hour: `${hour}:00 UTC`, + }); + + await ctx.reply(summary, { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [ + { + text: '✅ ' + ctx.i18n.t('continue'), + callback_data: 'sched_confirm', + }, + { text: '❌ ' + ctx.i18n.t('cancel'), callback_data: 'sched_cancel' }, + ], + ], + }, + }); +}; diff --git a/bot/modules/schedule/scenes.ts b/bot/modules/schedule/scenes.ts new file mode 100644 index 00000000..e32ce3bc --- /dev/null +++ b/bot/modules/schedule/scenes.ts @@ -0,0 +1,149 @@ +import { Scenes } from 'telegraf'; +import { logger } from '../../../logger'; +import { ScheduledOrder } from '../../../models'; +import { CommunityContext } from '../community/communityContext'; +import { UserDocument } from '../../../models/user'; +import * as messages from './messages'; +import { getRepublishCount, parseCustomDays, PRESET_DAYS } from './helpers'; + +export const SCHEDULE_ORDER = 'SCHEDULE_ORDER_WIZARD'; + +// Shape of the data carried in the wizard state for this scene. +interface ScheduleWizardState { + user: UserDocument; + scheduleType: string; + amount: number; + fiatAmount: number[]; + fiatCode: string; + paymentMethod: string; + priceMargin?: number | string; + awaitingCustomDays?: boolean; + days?: number[]; + hour?: number; +} + +const getState = (ctx: CommunityContext) => + ctx.wizard.state as unknown as ScheduleWizardState; + +export const scheduleOrderWizard = new Scenes.WizardScene( + SCHEDULE_ORDER, + // Step 0: runs on enter — ask for the days + async (ctx: CommunityContext) => { + try { + await messages.askScheduleDays(ctx); + return ctx.wizard.next(); + } catch (err) { + logger.error(err); + return ctx.scene.leave(); + } + }, + // Step 1: handle day selection (preset buttons, custom button, or custom text) + async (ctx: CommunityContext) => { + try { + const state = getState(ctx); + + // User pressed one of the day buttons + const data = (ctx.callbackQuery as any)?.data as string | undefined; + if (data) { + await ctx.answerCbQuery(); + if (data === 'sched_custom') { + state.awaitingCustomDays = true; + await messages.askCustomDays(ctx); + return; // stay on this step, waiting for the text + } + const presetKey = data.replace('sched_', ''); + if (Object.prototype.hasOwnProperty.call(PRESET_DAYS, presetKey)) { + state.days = PRESET_DAYS[presetKey]; + await messages.askScheduleHour(ctx); + return ctx.wizard.next(); + } + return; // ignore other callbacks + } + + // User typed the custom days + if (state.awaitingCustomDays) { + const text = (ctx.message as any)?.text?.trim(); + const days = text ? parseCustomDays(text) : null; + if (!days) { + await ctx.reply(ctx.i18n.t('invalid_days')); + return; // stay, reprompt + } + state.days = days; + await messages.askScheduleHour(ctx); + return ctx.wizard.next(); + } + } catch (err) { + logger.error(err); + return ctx.scene.leave(); + } + }, + // Step 2: handle hour input (text) + async (ctx: CommunityContext) => { + try { + const state = getState(ctx); + const text = (ctx.message as any)?.text?.trim(); + const hour = parseInt(text, 10); + if (isNaN(hour) || hour < 0 || hour > 23 || String(hour) !== text) { + await ctx.reply(ctx.i18n.t('invalid_hour')); + return; // stay, reprompt + } + state.hour = hour; + await messages.askScheduleConfirm(ctx, { + type: state.scheduleType, + fiatAmount: state.fiatAmount, + fiatCode: state.fiatCode, + paymentMethod: state.paymentMethod, + days: state.days!, + hour: state.hour, + }); + return ctx.wizard.next(); + } catch (err) { + logger.error(err); + return ctx.scene.leave(); + } + }, + // Step 3: handle confirm / cancel buttons + async (ctx: CommunityContext) => { + try { + const data = (ctx.callbackQuery as any)?.data as string | undefined; + if (!data) return; // ignore stray text, wait for a button + + await ctx.answerCbQuery(); + + if (data === 'sched_cancel') { + await ctx.reply(ctx.i18n.t('schedule_creation_cancelled')); + return ctx.scene.leave(); + } + + if (data !== 'sched_confirm') return; + + const state = getState(ctx); + if (state.days === undefined || state.hour === undefined) { + await ctx.reply(ctx.i18n.t('schedule_not_found')); + return ctx.scene.leave(); + } + + const schedule = await ScheduledOrder.create({ + creator_id: state.user._id, + type: state.scheduleType, + amount: state.amount, + fiat_amount: state.fiatAmount, + fiat_code: state.fiatCode, + payment_method: state.paymentMethod, + price_margin: Number(state.priceMargin) || 0, + days: state.days, + hour: state.hour, + republish_count: getRepublishCount(), + active: true, + }); + + await ctx.reply( + ctx.i18n.t('schedule_created', { scheduleId: schedule._id }), + ); + return ctx.scene.leave(); + } catch (err) { + logger.error(err); + return ctx.scene.leave(); + } + }, +); diff --git a/bot/start.ts b/bot/start.ts index 16594eb6..72e7d0d7 100644 --- a/bot/start.ts +++ b/bot/start.ts @@ -35,6 +35,7 @@ import * as OrdersModule from './modules/orders'; import * as UserModule from './modules/user'; import * as DisputeModule from './modules/dispute'; import * as BlockModule from './modules/block'; +import * as ScheduleModule from './modules/schedule'; import { rateUser, cancelAddInvoice, @@ -72,6 +73,7 @@ import { nodeInfo, checkSolvers, checkHoldInvoiceExpired, + publishScheduledOrders, } from '../jobs'; import { logger } from '../logger'; import { IUsernameId } from '../models/community'; @@ -237,6 +239,11 @@ const initialize = ( await checkHoldInvoiceExpired(bot); }); + // Publish scheduled orders — runs at the top of every hour (UTC) + schedule.scheduleJob(`0 * * * *`, async () => { + await publishScheduledOrders(bot); + }); + bot.start(async (ctx: MainContext) => { try { if (!('message' in ctx.update) || !('text' in ctx.update.message)) { @@ -300,6 +307,7 @@ const initialize = ( CommunityModule.configure(bot); LanguageModule.configure(bot); BlockModule.configure(bot); + ScheduleModule.configure(bot); bot.command('release', userMiddleware, async ctx => { try { diff --git a/jobs/index.ts b/jobs/index.ts index fce4cbc7..cb22db53 100644 --- a/jobs/index.ts +++ b/jobs/index.ts @@ -9,6 +9,7 @@ import deleteCommunity from './communities'; import nodeInfo from './node_info'; import checkSolvers from './check_solvers'; import checkHoldInvoiceExpired from './check_hold_invoice_expired'; +import publishScheduledOrders from './scheduled_orders'; export { attemptPendingPayments, @@ -20,4 +21,5 @@ export { nodeInfo, checkSolvers, checkHoldInvoiceExpired, + publishScheduledOrders, }; diff --git a/jobs/scheduled_orders.ts b/jobs/scheduled_orders.ts new file mode 100644 index 00000000..c8e8f244 --- /dev/null +++ b/jobs/scheduled_orders.ts @@ -0,0 +1,156 @@ +import { Telegraf } from 'telegraf'; + +import { ScheduledOrder, User } from '../models'; +import { getUserI18nContext } from '../util'; +import { logger } from '../logger'; +import { CommunityContext } from '../bot/modules/community/communityContext'; +import * as ordersActions from '../bot/ordersActions'; +import { + publishBuyOrderMessage, + publishSellOrderMessage, +} from '../bot/messages'; +import { + getDormantLimit, + isDormantMaker, +} from '../bot/modules/schedule/helpers'; + +// Runs every hour. For each active ScheduledOrder whose days/hour (UTC) match +// now, publishes a brand-new Order from the stored mold. The mold is never +// mutated into an order; each publication has its own id and lifecycle. +const publishScheduledOrders = async (bot: Telegraf) => { + try { + const now = new Date(); + const currentDay = now.getUTCDay(); // 0 (Sunday) .. 6 (Saturday) + const currentHour = now.getUTCHours(); // 0 .. 23 + + const schedules = await ScheduledOrder.find({ + active: true, + days: currentDay, + hour: currentHour, + }); + + // Makers already handled as dormant in this run, so we don't deactivate + // their schedules or notify them more than once. + const dormantHandled = new Set(); + + for (const schedule of schedules) { + try { + if (dormantHandled.has(schedule.creator_id)) continue; + + if (schedule.republish_count <= 0) { + schedule.active = false; + await schedule.save(); + continue; + } + + const user = await User.findById(schedule.creator_id); + if (!user) { + logger.warning( + `ScheduledOrder ${schedule._id}: creator ${schedule.creator_id} not found, deactivating`, + ); + schedule.active = false; + await schedule.save(); + continue; + } + + const i18n = await getUserI18nContext(user); + + // Dormant maker enforcement: if the creator has left their last N taken + // orders uncompleted, remove all their schedules and notify them. + if (await isDormantMaker(schedule.creator_id)) { + const removed = await ScheduledOrder.updateMany( + { creator_id: schedule.creator_id, active: true }, + { active: false }, + ); + dormantHandled.add(schedule.creator_id); + try { + await bot.telegram.sendMessage( + user.tg_id, + i18n.t('schedule_dormant_removed', { count: getDormantLimit() }), + ); + } catch (error) { + logger.warning( + `ScheduledOrder: failed to notify dormant maker ${schedule.creator_id}: ${String(error)}`, + ); + } + logger.info( + `ScheduledOrder: deactivated ${removed.modifiedCount} schedule(s) for dormant maker ${schedule.creator_id}`, + ); + continue; + } + + const order = await ordersActions.createOrder(i18n, bot, user, { + type: schedule.type, + amount: schedule.amount, + fiatAmount: schedule.fiat_amount, + fiatCode: schedule.fiat_code, + paymentMethod: schedule.payment_method, + status: 'PENDING', + priceMargin: schedule.price_margin, + community_id: schedule.community_id, + }); + + if (!order) { + logger.warning( + `ScheduledOrder ${schedule._id}: failed to create order`, + ); + continue; + } + + // Reserve the cycle atomically BEFORE the external publish. The guard + // on republish_count acts as optimistic concurrency control: only one + // run can decrement from this exact value. If the process dies after + // reserving but before/while publishing, the schedule has already + // advanced, so the next tick will not republish the same cycle. We + // prefer losing one publication over posting a duplicate order. + const remaining = schedule.republish_count - 1; + const reserved = await ScheduledOrder.findOneAndUpdate( + { + _id: schedule._id, + active: true, + republish_count: schedule.republish_count, + }, + { + last_order_id: order._id, + republish_count: remaining, + active: remaining > 0, + }, + { new: true }, + ); + + if (!reserved) { + // Another run already claimed this cycle; drop the order we created. + logger.warning( + `ScheduledOrder ${schedule._id}: cycle already claimed, skipping publish`, + ); + continue; + } + + const publishFn = + schedule.type === 'buy' + ? publishBuyOrderMessage + : publishSellOrderMessage; + await publishFn(bot, user, order, i18n, false); + + // publishFn swallows errors and returns void; surface a failed publish + // for observability, but the cycle is already reserved either way. + if (order.status === 'CLOSED' || !order.tg_channel_message1) { + logger.warning( + `ScheduledOrder ${schedule._id}: publish failed after reservation`, + ); + continue; + } + + logger.info( + `ScheduledOrder ${schedule._id} published order ${order._id}, ${remaining} cycles left`, + ); + } catch (error) { + logger.error(`publishScheduledOrders inner error: ${String(error)}`); + } + } + } catch (error) { + logger.error(`publishScheduledOrders error: ${String(error)}`); + } +}; + +export default publishScheduledOrders; diff --git a/locales/de.yaml b/locales/de.yaml index 36dfbaba..a4db5ab1 100644 --- a/locales/de.yaml +++ b/locales/de.yaml @@ -261,6 +261,10 @@ help: | /blocklist - Liste Ihrer blockierten Benutzer /cancel <_order id_> - Einen Auftrag stornieren, die noch nicht angenommen wurde /cancelall - Storniere alle aufgegebenen und nicht angenommenen Aufträge + /scheduleorder <_buy|sell_> <_sats_> <_Fiat-Betrag_> <_Fiat-Code_> <_Zahlungsmethode_> [_Aufschlag_] - Erstellt einen wiederkehrenden Auftrag, der automatisch neu veröffentlicht wird + /cancelschedule <_Schedule-ID_> - Stoppt die automatische Neuveröffentlichung eines geplanten Auftrags + /listschedules - Listet deine aktiven geplanten Aufträge mit ihren IDs auf + /cancelallschedules - Bricht alle deine geplanten Aufträge ab /terms - Zeigt die Allgemeinen Geschäftsbedingungen bei der Nutzung des Bots an Nostr: @@ -725,3 +729,42 @@ community_enabled_by_admin: Ein Administrator hat deine Community ${communityNam community_already_disabled: Diese Community ist bereits deaktiviert community_already_enabled: Diese Community ist bereits aktiviert ambiguous_community_input: Mehrere Communities stimmen mit diesem Benutzernamen überein, bitte verwende stattdessen die Community-ID + +scheduleorder_usage: "Verwendung: /scheduleorder [Aufschlag]" +cancelschedule_usage: "Verwendung: /cancelschedule " +schedule_not_found: "⚠️ Schedule nicht gefunden oder bereits inaktiv." +schedule_cancelled: "✅ Schedule ${scheduleId} abgebrochen. Es werden keine Bestellungen mehr automatisch veröffentlicht." +listschedules_empty: "Du hast keine aktiven geplanten Aufträge." +listschedules_header: "📋 Deine geplanten Aufträge:" +schedule_list_item: | + 🆔 `${scheduleId}` + ${type} · ${fiatAmount} ${fiatCode} · ${paymentMethod} + 📅 ${days} · 🕐 ${hour} +cancelallschedules_none: "Du hast keine aktiven geplanten Aufträge zum Abbrechen." +all_schedules_cancelled: "✅ ${count} geplante(r) Auftrag/Aufträge abgebrochen. Es werden keine Bestellungen mehr automatisch veröffentlicht." +schedule_req_max_reached: "⚠️ Du hast bereits ${max} aktive geplante Aufträge, das ist das erlaubte Maximum. Brich einen mit /cancelschedule ab, bevor du einen neuen erstellst." +schedule_req_account_age: "⚠️ Dein Konto muss mindestens ${days} Tage alt sein, um geplante Aufträge zu erstellen." +schedule_req_completed_orders: "⚠️ Du benötigst mindestens ${count} abgeschlossene Aufträge, um geplante Aufträge zu erstellen." +schedule_req_volume: "⚠️ Du benötigst ein gehandeltes Volumen von mindestens ${volume} Sats, um geplante Aufträge zu erstellen." +schedule_req_rating: "⚠️ Du benötigst eine Reputation von mindestens ${rating}, um geplante Aufträge zu erstellen." +schedule_req_completion_rate: "⚠️ Deine Auftrags-Abschlussquote (${current}%) liegt unter den erforderlichen ${rate}%. Schließe mehr deiner Aufträge ab, bevor du planst." +schedule_dormant_removed: "⚠️ Ich habe alle deine geplanten Aufträge entfernt, weil du deine letzten ${count} angenommenen Aufträge nicht abgeschlossen hast." +schedule_creation_cancelled: "❌ Erstellung des Schedules abgebrochen." +schedule_created: "✅ Geplante Bestellung erstellt! Deine ID ist: ${scheduleId}\nVerwende /cancelschedule ${scheduleId} zum Stoppen." +schedule_choose_days: "📅 An welchen Tagen soll die Bestellung veröffentlicht werden?" +sched_daily: Jeden Tag +sched_weekdays: Mo–Fr +sched_weekend: Wochenende +sched_custom: Benutzerdefiniert +schedule_enter_custom_days: "✍️ Gib die Tage auf Englisch durch Kommas getrennt ein (z.B.: sunday, wednesday, friday)" +invalid_days: "⚠️ Ungültige Tage. Gib englische Tagnamen durch Kommas getrennt ein (z.B.: sunday, wednesday, friday)." +schedule_enter_hour: "🕐 Zu welcher Uhrzeit soll die Bestellung veröffentlicht werden? Gib eine Zahl zwischen 0 und 23 ein (UTC)." +invalid_hour: "⚠️ Ungültige Stunde. Bitte gib eine Zahl zwischen 0 und 23 ein." +schedule_confirm_summary: | + 📋 *Zusammenfassung der geplanten Bestellung* + + Typ: ${type} + Betrag: ${fiatAmount} ${fiatCode} + Zahlungsmethode: ${paymentMethod} + Tage: ${days} + Uhrzeit: ${hour} diff --git a/locales/en.yaml b/locales/en.yaml index 83b202c2..701c7ade 100644 --- a/locales/en.yaml +++ b/locales/en.yaml @@ -265,6 +265,10 @@ help: | /blocklist - List of your blocked users /cancel <_order id_> - Cancel an order that has not been taken yet /cancelall - Cancel all posted and untaken orders + /scheduleorder <_buy|sell_> <_sats_> <_fiat amount_> <_fiat code_> <_payment method_> [_premium_] - Creates a recurring order that auto-republishes + /cancelschedule <_schedule id_> - Stops auto-republishing of a scheduled order + /listschedules - Lists your active scheduled orders with their IDs + /cancelallschedules - Cancels all your scheduled orders /terms - Shows the terms and conditions when using the bot Nostr: @@ -731,3 +735,43 @@ community_enabled_by_admin: An administrator has re-enabled your community ${com community_already_disabled: This community is already disabled community_already_enabled: This community is already enabled ambiguous_community_input: Multiple communities match that username, please use the community ID instead +scheduleorder_usage: "Usage: /scheduleorder [premium]" +cancelschedule_usage: "Usage: /cancelschedule " +schedule_not_found: "⚠️ Schedule not found or already inactive." +schedule_cancelled: "✅ Schedule ${scheduleId} cancelled. No more orders will be auto-published." +listschedules_empty: "You have no active scheduled orders." +listschedules_header: "📋 Your scheduled orders:" +schedule_list_item: | + 🆔 `${scheduleId}` + ${type} · ${fiatAmount} ${fiatCode} · ${paymentMethod} + 📅 ${days} · 🕐 ${hour} +cancelallschedules_none: "You have no active scheduled orders to cancel." +all_schedules_cancelled: "✅ Cancelled ${count} scheduled order(s). No more orders will be auto-published." +schedule_req_max_reached: "⚠️ You already have ${max} active scheduled orders, which is the maximum allowed. Cancel one with /cancelschedule before creating a new one." +schedule_req_account_age: "⚠️ Your account must be at least ${days} days old to create scheduled orders." +schedule_req_completed_orders: "⚠️ You need at least ${count} completed orders to create scheduled orders." +schedule_req_volume: "⚠️ You need a traded volume of at least ${volume} sats to create scheduled orders." +schedule_req_rating: "⚠️ You need a reputation of at least ${rating} to create scheduled orders." +schedule_req_completion_rate: "⚠️ Your order completion rate (${current}%) is below the required ${rate}%. Complete more of your orders before scheduling." +schedule_dormant_removed: "⚠️ I removed all your scheduled orders because you didn't complete your last ${count} taken orders." +schedule_creation_cancelled: "❌ Schedule creation cancelled." +schedule_created: "✅ Scheduled order created! Your ID is: ${scheduleId}\nUse /cancelschedule ${scheduleId} to stop it." +schedule_choose_days: "📅 On which days should the order be published?" +sched_daily: Every day +sched_weekdays: Mon–Fri +sched_weekend: Weekend +sched_custom: Custom +schedule_enter_custom_days: "✍️ Enter the days separated by commas (e.g.: sunday, wednesday, friday)" +invalid_days: "⚠️ Invalid days. Enter English day names separated by commas (e.g.: sunday, wednesday, friday)." +schedule_enter_hour: "🕐 At what hour should the order be published? Enter a number between 0 and 23 (UTC)." +invalid_hour: "⚠️ Invalid hour. Please enter a number between 0 and 23." +schedule_confirm_summary: | + 📋 *Order schedule summary* + + Type: ${type} + Amount: ${fiatAmount} ${fiatCode} + Payment method: ${paymentMethod} + Days: ${days} + Hour: ${hour} + + Confirm? diff --git a/locales/es.yaml b/locales/es.yaml index e54b73a3..db3623fd 100644 --- a/locales/es.yaml +++ b/locales/es.yaml @@ -262,6 +262,10 @@ help: | /blocklist - Listado de tus usuarios bloqueados /cancel <_id orden_> - Cancela una orden que no ha sido tomada /cancelall - Cancela todas las órdenes publicadas y que no han sido tomadas + /scheduleorder <_buy|sell_> <_sats_> <_monto fiat_> <_código fiat_> <_método de pago_> [_prima_] - Crea una orden recurrente que se republica automáticamente + /cancelschedule <_id del schedule_> - Detiene la republicación automática de una orden programada + /listschedules - Lista tus órdenes programadas activas con sus IDs + /cancelallschedules - Cancela todas tus órdenes programadas /terms - Muestra los términos y condiciones al usar el bot /privacy - Muestra la Política de Privacidad @@ -728,3 +732,42 @@ community_enabled_by_admin: Un administrador ha reactivado tu comunidad ${commun community_already_disabled: Esta comunidad ya está deshabilitada community_already_enabled: Esta comunidad ya está habilitada ambiguous_community_input: Varias comunidades coinciden con ese nombre de usuario, por favor usa el ID de la comunidad + +scheduleorder_usage: "Uso: /scheduleorder [prima]" +cancelschedule_usage: "Uso: /cancelschedule " +schedule_not_found: "⚠️ Schedule no encontrado o ya inactivo." +schedule_cancelled: "✅ Schedule ${scheduleId} cancelado. No se publicarán más órdenes automáticamente." +listschedules_empty: "No tenés órdenes programadas activas." +listschedules_header: "📋 Tus órdenes programadas:" +schedule_list_item: | + 🆔 `${scheduleId}` + ${type} · ${fiatAmount} ${fiatCode} · ${paymentMethod} + 📅 ${days} · 🕐 ${hour} +cancelallschedules_none: "No tenés órdenes programadas activas para cancelar." +all_schedules_cancelled: "✅ Se cancelaron ${count} orden(es) programada(s). No se publicarán más órdenes automáticamente." +schedule_req_max_reached: "⚠️ Ya tenés ${max} órdenes programadas activas, que es el máximo permitido. Cancelá una con /cancelschedule antes de crear una nueva." +schedule_req_account_age: "⚠️ Tu cuenta debe tener al menos ${days} días de antigüedad para crear órdenes programadas." +schedule_req_completed_orders: "⚠️ Necesitás al menos ${count} órdenes completadas para crear órdenes programadas." +schedule_req_volume: "⚠️ Necesitás un volumen operado de al menos ${volume} sats para crear órdenes programadas." +schedule_req_rating: "⚠️ Necesitás una reputación de al menos ${rating} para crear órdenes programadas." +schedule_req_completion_rate: "⚠️ Tu tasa de completitud de órdenes (${current}%) está por debajo del ${rate}% requerido. Completá más de tus órdenes antes de programar." +schedule_dormant_removed: "⚠️ Eliminé todas tus órdenes programadas porque no completaste tus últimas ${count} órdenes tomadas." +schedule_creation_cancelled: "❌ Creación de schedule cancelada." +schedule_created: "✅ Orden programada creada. Tu ID es: ${scheduleId}\nUsa /cancelschedule ${scheduleId} para detenerla." +schedule_choose_days: "📅 ¿En qué días debe publicarse la orden?" +sched_daily: Todos los días +sched_weekdays: Lun–Vie +sched_weekend: Fin de semana +sched_custom: Personalizado +schedule_enter_custom_days: "✍️ Ingresa los días separados por comas en inglés (ej: sunday, wednesday, friday)" +invalid_days: "⚠️ Días inválidos. Ingresá los nombres de los días en inglés separados por comas (ej: sunday, wednesday, friday)." +schedule_enter_hour: "🕐 ¿A qué hora debe publicarse la orden? Ingresa un número entre 0 y 23 (UTC)." +invalid_hour: "⚠️ Hora inválida. Por favor ingresa un número entre 0 y 23." +schedule_confirm_summary: | + 📋 *Resumen de la orden programada* + + Tipo: ${type} + Monto: ${fiatAmount} ${fiatCode} + Método de pago: ${paymentMethod} + Días: ${days} + Hora: ${hour} diff --git a/locales/fa.yaml b/locales/fa.yaml index 002f6e87..317666b1 100644 --- a/locales/fa.yaml +++ b/locales/fa.yaml @@ -314,6 +314,18 @@ help: | /cancelall تمامی سفارش‌های منتشر شده و پذیرفته نشده را لغو می‌کند. + /scheduleorder <_buy|sell_> <_sats_> <_مبلغ فیات_> <_کد فیات_> <_روش پرداخت_> [_پریمیوم_] + یک سفارش تکرارشونده ایجاد می‌کند که به‌صورت خودکار بازنشر می‌شود. + + /cancelschedule <_شناسه زمان‌بندی_> + بازنشر خودکار سفارش زمان‌بندی‌شده را متوقف می‌کند. + + /listschedules + سفارش‌های زمان‌بندی‌شده فعال شما را همراه با شناسه‌هایشان فهرست می‌کند. + + /cancelallschedules + تمام سفارش‌های زمان‌بندی‌شده شما را لغو می‌کند. + /terms شرایط و ضوابط استفاده از ربات را نمایش می‌دهد. @@ -824,3 +836,42 @@ community_enabled_by_admin: یک مدیر جامعه ${communityName} شما ر community_already_disabled: این جامعه از قبل غیرفعال است community_already_enabled: این جامعه از قبل فعال است ambiguous_community_input: چندین جامعه با این نام کاربری مطابقت دارند، لطفاً از شناسه جامعه استفاده کنید + +scheduleorder_usage: "راهنما: /scheduleorder <سات> <مبلغ فیات> <کد فیات> <روش پرداخت> [پریمیوم]" +cancelschedule_usage: "راهنما: /cancelschedule <شناسه زمان‌بندی>" +schedule_not_found: "⚠️ زمان‌بندی پیدا نشد یا قبلاً غیرفعال شده است." +schedule_cancelled: "✅ زمان‌بندی ${scheduleId} لغو شد. دیگر هیچ سفارشی به‌صورت خودکار منتشر نخواهد شد." +listschedules_empty: "شما هیچ سفارش زمان‌بندی‌شده فعالی ندارید." +listschedules_header: "📋 سفارش‌های زمان‌بندی‌شده شما:" +schedule_list_item: | + 🆔 `${scheduleId}` + ${type} · ${fiatAmount} ${fiatCode} · ${paymentMethod} + 📅 ${days} · 🕐 ${hour} +cancelallschedules_none: "شما هیچ سفارش زمان‌بندی‌شده فعالی برای لغو ندارید." +all_schedules_cancelled: "✅ ${count} سفارش زمان‌بندی‌شده لغو شد. دیگر هیچ سفارشی به‌صورت خودکار منتشر نخواهد شد." +schedule_req_max_reached: "⚠️ شما در حال حاضر ${max} سفارش زمان‌بندی‌شده فعال دارید که حداکثر مجاز است. پیش از ساختن سفارش جدید، یکی را با /cancelschedule لغو کنید." +schedule_req_account_age: "⚠️ برای ساختن سفارش‌های زمان‌بندی‌شده، حساب شما باید حداقل ${days} روز قدمت داشته باشد." +schedule_req_completed_orders: "⚠️ برای ساختن سفارش‌های زمان‌بندی‌شده به حداقل ${count} سفارش تکمیل‌شده نیاز دارید." +schedule_req_volume: "⚠️ برای ساختن سفارش‌های زمان‌بندی‌شده به حجم معاملاتی حداقل ${volume} ساتوشی نیاز دارید." +schedule_req_rating: "⚠️ برای ساختن سفارش‌های زمان‌بندی‌شده به اعتبار حداقل ${rating} نیاز دارید." +schedule_req_completion_rate: "⚠️ نرخ تکمیل سفارش‌های شما (${current}%) کمتر از ${rate}% موردنیاز است. پیش از زمان‌بندی، سفارش‌های بیشتری را تکمیل کنید." +schedule_dormant_removed: "⚠️ همه سفارش‌های زمان‌بندی‌شده شما را حذف کردم، زیرا آخرین ${count} سفارش پذیرفته‌شده خود را تکمیل نکردید." +schedule_creation_cancelled: "❌ ایجاد زمان‌بندی لغو شد." +schedule_created: "✅ سفارش زمان‌بندی‌شده ایجاد شد! شناسه شما: ${scheduleId}\nبرای توقف از /cancelschedule ${scheduleId} استفاده کنید." +schedule_choose_days: "📅 در چه روزهایی سفارش منتشر شود؟" +sched_daily: هر روز +sched_weekdays: دوشنبه–جمعه +sched_weekend: آخر هفته +sched_custom: سفارشی +schedule_enter_custom_days: "✍️ روزها را به انگلیسی و با کاما وارد کنید (مثال: sunday, wednesday, friday)" +invalid_days: "⚠️ روزهای نامعتبر. نام روزها را به انگلیسی با کاما جدا کنید (مثال: sunday, wednesday, friday)." +schedule_enter_hour: "🕐 سفارش در چه ساعتی منتشر شود؟ عددی بین ۰ تا ۲۳ وارد کنید (UTC)." +invalid_hour: "⚠️ ساعت نامعتبر است. لطفاً عددی بین ۰ تا ۲۳ وارد کنید." +schedule_confirm_summary: | + 📋 *خلاصه سفارش زمان‌بندی‌شده* + + نوع: ${type} + مبلغ: ${fiatAmount} ${fiatCode} + روش پرداخت: ${paymentMethod} + روزها: ${days} + ساعت: ${hour} diff --git a/locales/fr.yaml b/locales/fr.yaml index a6decd9b..ebe15f0c 100644 --- a/locales/fr.yaml +++ b/locales/fr.yaml @@ -263,6 +263,10 @@ help: | /blocklist - Liste de vos utilisateurs bloqués /cancel <_order id_> - Annule une commande qui n'a pas encore été prise /cancelall - Annule toutes les commandes validées et non prises + /scheduleorder <_buy|sell_> <_sats_> <_montant fiat_> <_code fiat_> <_méthode de paiement_> [_prime_] - Crée une commande récurrente qui se republie automatiquement + /cancelschedule <_id du schedule_> - Arrête la republication automatique d'une commande planifiée + /listschedules - Liste vos commandes planifiées actives avec leurs identifiants + /cancelallschedules - Annule toutes vos commandes planifiées /terms - Affiche les termes et conditions lors de l'utilisation du bot Nostr: @@ -724,3 +728,42 @@ community_enabled_by_admin: Un administrateur a réactivé votre communauté ${c community_already_disabled: Cette communauté est déjà désactivée community_already_enabled: Cette communauté est déjà activée ambiguous_community_input: Plusieurs communautés correspondent à ce nom d'utilisateur, veuillez utiliser l'ID de la communauté à la place + +scheduleorder_usage: "Utilisation : /scheduleorder [prime]" +cancelschedule_usage: "Utilisation : /cancelschedule " +schedule_not_found: "⚠️ Schedule introuvable ou déjà inactif." +schedule_cancelled: "✅ Schedule ${scheduleId} annulé. Aucune autre commande ne sera publiée automatiquement." +listschedules_empty: "Vous n'avez aucune commande planifiée active." +listschedules_header: "📋 Vos commandes planifiées :" +schedule_list_item: | + 🆔 `${scheduleId}` + ${type} · ${fiatAmount} ${fiatCode} · ${paymentMethod} + 📅 ${days} · 🕐 ${hour} +cancelallschedules_none: "Vous n'avez aucune commande planifiée active à annuler." +all_schedules_cancelled: "✅ ${count} commande(s) planifiée(s) annulée(s). Aucune autre commande ne sera publiée automatiquement." +schedule_req_max_reached: "⚠️ Vous avez déjà ${max} commandes planifiées actives, ce qui est le maximum autorisé. Annulez-en une avec /cancelschedule avant d'en créer une nouvelle." +schedule_req_account_age: "⚠️ Votre compte doit exister depuis au moins ${days} jours pour créer des commandes planifiées." +schedule_req_completed_orders: "⚠️ Vous devez avoir au moins ${count} commandes terminées pour créer des commandes planifiées." +schedule_req_volume: "⚠️ Vous devez avoir un volume échangé d'au moins ${volume} sats pour créer des commandes planifiées." +schedule_req_rating: "⚠️ Vous devez avoir une réputation d'au moins ${rating} pour créer des commandes planifiées." +schedule_req_completion_rate: "⚠️ Votre taux de complétion des commandes (${current}%) est inférieur aux ${rate}% requis. Terminez davantage de vos commandes avant de planifier." +schedule_dormant_removed: "⚠️ J'ai supprimé toutes vos commandes planifiées car vous n'avez pas terminé vos ${count} dernières commandes acceptées." +schedule_creation_cancelled: "❌ Création du schedule annulée." +schedule_created: "✅ Ordre planifié créé ! Votre ID est : ${scheduleId}\nUtilisez /cancelschedule ${scheduleId} pour l'arrêter." +schedule_choose_days: "📅 Quels jours l'ordre doit-il être publié ?" +sched_daily: Tous les jours +sched_weekdays: Lun–Ven +sched_weekend: Week-end +sched_custom: Personnalisé +schedule_enter_custom_days: "✍️ Entrez les jours en anglais séparés par des virgules (ex : sunday, wednesday, friday)" +invalid_days: "⚠️ Jours invalides. Entrez les noms des jours en anglais séparés par des virgules (ex : sunday, wednesday, friday)." +schedule_enter_hour: "🕐 À quelle heure l'ordre doit-il être publié ? Entrez un nombre entre 0 et 23 (UTC)." +invalid_hour: "⚠️ Heure invalide. Veuillez entrer un nombre entre 0 et 23." +schedule_confirm_summary: | + 📋 *Résumé de l'ordre planifié* + + Type : ${type} + Montant : ${fiatAmount} ${fiatCode} + Méthode de paiement : ${paymentMethod} + Jours : ${days} + Heure : ${hour} diff --git a/locales/it.yaml b/locales/it.yaml index 0e47fe04..f117866b 100644 --- a/locales/it.yaml +++ b/locales/it.yaml @@ -261,6 +261,10 @@ help: | /blocklist - Elenco degli utenti bloccati /cancel <_order id_> - Cancella un ordine che non è ancora stato accettato /cancelall - Cancella tutti gli ordini non ancora accettati + /scheduleorder <_buy|sell_> <_sats_> <_importo fiat_> <_codice fiat_> <_metodo di pagamento_> [_premio_] - Crea un ordine ricorrente che si ripubblica automaticamente + /cancelschedule <_id dello schedule_> - Ferma la ripubblicazione automatica di un ordine programmato + /listschedules - Elenca i tuoi ordini programmati attivi con i loro ID + /cancelallschedules - Annulla tutti i tuoi ordini programmati /terms: mostra i termini e le condizioni quando si utilizza il bot Nostr: @@ -722,3 +726,42 @@ community_enabled_by_admin: Un amministratore ha riabilitato la tua comunità ${ community_already_disabled: Questa comunità è già disabilitata community_already_enabled: Questa comunità è già abilitata ambiguous_community_input: Più comunità corrispondono a quel nome utente, usa invece l'ID della comunità + +scheduleorder_usage: "Utilizzo: /scheduleorder [premio]" +cancelschedule_usage: "Utilizzo: /cancelschedule " +schedule_not_found: "⚠️ Schedule non trovato o già inattivo." +schedule_cancelled: "✅ Schedule ${scheduleId} annullato. Non verranno più pubblicate offerte automaticamente." +listschedules_empty: "Non hai ordini programmati attivi." +listschedules_header: "📋 I tuoi ordini programmati:" +schedule_list_item: | + 🆔 `${scheduleId}` + ${type} · ${fiatAmount} ${fiatCode} · ${paymentMethod} + 📅 ${days} · 🕐 ${hour} +cancelallschedules_none: "Non hai ordini programmati attivi da annullare." +all_schedules_cancelled: "✅ Annullati ${count} ordine/i programmato/i. Non verranno più pubblicate offerte automaticamente." +schedule_req_max_reached: "⚠️ Hai già ${max} ordini programmati attivi, che è il massimo consentito. Annullane uno con /cancelschedule prima di crearne uno nuovo." +schedule_req_account_age: "⚠️ Il tuo account deve avere almeno ${days} giorni per creare ordini programmati." +schedule_req_completed_orders: "⚠️ Ti servono almeno ${count} ordini completati per creare ordini programmati." +schedule_req_volume: "⚠️ Ti serve un volume scambiato di almeno ${volume} sats per creare ordini programmati." +schedule_req_rating: "⚠️ Ti serve una reputazione di almeno ${rating} per creare ordini programmati." +schedule_req_completion_rate: "⚠️ Il tuo tasso di completamento degli ordini (${current}%) è inferiore al ${rate}% richiesto. Completa più dei tuoi ordini prima di programmare." +schedule_dormant_removed: "⚠️ Ho rimosso tutti i tuoi ordini programmati perché non hai completato i tuoi ultimi ${count} ordini presi." +schedule_creation_cancelled: "❌ Creazione dello schedule annullata." +schedule_created: "✅ Ordine programmato creato! Il tuo ID è: ${scheduleId}\nUsa /cancelschedule ${scheduleId} per fermarlo." +schedule_choose_days: "📅 In quali giorni deve essere pubblicato l'ordine?" +sched_daily: Ogni giorno +sched_weekdays: Lun–Ven +sched_weekend: Weekend +sched_custom: Personalizzato +schedule_enter_custom_days: "✍️ Inserisci i giorni in inglese separati da virgole (es: sunday, wednesday, friday)" +invalid_days: "⚠️ Giorni non validi. Inserisci i nomi dei giorni in inglese separati da virgole (es: sunday, wednesday, friday)." +schedule_enter_hour: "🕐 A che ora deve essere pubblicato l'ordine? Inserisci un numero tra 0 e 23 (UTC)." +invalid_hour: "⚠️ Ora non valida. Inserisci un numero tra 0 e 23." +schedule_confirm_summary: | + 📋 *Riepilogo ordine programmato* + + Tipo: ${type} + Importo: ${fiatAmount} ${fiatCode} + Metodo di pagamento: ${paymentMethod} + Giorni: ${days} + Ora: ${hour} diff --git a/locales/ko.yaml b/locales/ko.yaml index 03c9dafe..448b7409 100644 --- a/locales/ko.yaml +++ b/locales/ko.yaml @@ -263,6 +263,10 @@ help: | /terms - 사용자 이용 약관 및 면책 조항을 표시합니다. /cancel <_주문 id_> - 아직 수락되지 않은 주문을 취소합니다. /cancelall - 등록되었지만 아직 수락되지 않은 모든 주문들을 취소합니다. + /scheduleorder <_buy|sell_> <_sats_> <_법정화폐 금액_> <_법정화폐 코드_> <_결제 방법_> [_프리미엄_] - 자동으로 재게시되는 반복 주문을 생성합니다 + /cancelschedule <_schedule id_> - 예약 주문의 자동 재게시를 중지합니다 + /listschedules - 활성 예약 주문과 해당 ID를 나열합니다 + /cancelallschedules - 모든 예약 주문을 취소합니다 Nostr: /setnpub <_노스터 npub_> - 노스터 공개키를 설정합니다. 이 명령어는 /settings 명령어 수행 중에만 사용 가능합니다. @@ -722,3 +726,42 @@ community_enabled_by_admin: 관리자가 커뮤니티 ${communityName}을(를) community_already_disabled: 이 커뮤니티는 이미 비활성화되어 있습니다 community_already_enabled: 이 커뮤니티는 이미 활성화되어 있습니다 ambiguous_community_input: 해당 사용자 이름과 일치하는 커뮤니티가 여러 개 있습니다. 커뮤니티 ID를 사용해 주세요 + +scheduleorder_usage: "사용법: /scheduleorder <법정화폐 금액> <법정화폐 코드> <결제 방법> [프리미엄]" +cancelschedule_usage: "사용법: /cancelschedule " +schedule_not_found: "⚠️ 스케줄을 찾을 수 없거나 이미 비활성화되었습니다." +schedule_cancelled: "✅ 스케줄 ${scheduleId}이(가) 취소되었습니다. 더 이상 자동으로 주문이 게시되지 않습니다." +listschedules_empty: "활성화된 예약 주문이 없습니다." +listschedules_header: "📋 예약 주문 목록:" +schedule_list_item: | + 🆔 `${scheduleId}` + ${type} · ${fiatAmount} ${fiatCode} · ${paymentMethod} + 📅 ${days} · 🕐 ${hour} +cancelallschedules_none: "취소할 활성 예약 주문이 없습니다." +all_schedules_cancelled: "✅ ${count}개의 예약 주문이 취소되었습니다. 더 이상 자동으로 주문이 게시되지 않습니다." +schedule_req_max_reached: "⚠️ 이미 활성 예약 주문이 ${max}개 있으며, 이는 최대 허용 수입니다. 새로 만들기 전에 /cancelschedule 로 하나를 취소하세요." +schedule_req_account_age: "⚠️ 예약 주문을 생성하려면 계정이 최소 ${days}일 이상이어야 합니다." +schedule_req_completed_orders: "⚠️ 예약 주문을 생성하려면 완료된 주문이 최소 ${count}건 필요합니다." +schedule_req_volume: "⚠️ 예약 주문을 생성하려면 최소 ${volume} sats의 거래량이 필요합니다." +schedule_req_rating: "⚠️ 예약 주문을 생성하려면 평판이 최소 ${rating} 이상이어야 합니다." +schedule_req_completion_rate: "⚠️ 주문 완료율(${current}%)이 요구되는 ${rate}%보다 낮습니다. 예약하기 전에 더 많은 주문을 완료하세요." +schedule_dormant_removed: "⚠️ 최근 ${count}건의 수락된 주문을 완료하지 않아 모든 예약 주문을 삭제했습니다." +schedule_creation_cancelled: "❌ 스케줄 생성이 취소되었습니다." +schedule_created: "✅ 예약 주문이 생성되었습니다! ID: ${scheduleId}\n중지하려면 /cancelschedule ${scheduleId}을 사용하세요." +schedule_choose_days: "📅 어떤 요일에 주문을 게시할까요?" +sched_daily: 매일 +sched_weekdays: 월–금 +sched_weekend: 주말 +sched_custom: 직접 입력 +schedule_enter_custom_days: "✍️ 요일을 영어로 쉼표 구분하여 입력하세요 (예: sunday, wednesday, friday)" +invalid_days: "⚠️ 유효하지 않은 요일입니다. 영어 요일 이름을 쉼표로 구분하여 입력하세요 (예: sunday, wednesday, friday)." +schedule_enter_hour: "🕐 주문을 게시할 시간을 입력하세요. 0에서 23 사이의 숫자를 입력하세요 (UTC)." +invalid_hour: "⚠️ 유효하지 않은 시간입니다. 0에서 23 사이의 숫자를 입력하세요." +schedule_confirm_summary: | + 📋 *예약 주문 요약* + + 유형: ${type} + 금액: ${fiatAmount} ${fiatCode} + 결제 방법: ${paymentMethod} + 요일: ${days} + 시간: ${hour} diff --git a/locales/pt.yaml b/locales/pt.yaml index 991d31e6..1fd81f7b 100644 --- a/locales/pt.yaml +++ b/locales/pt.yaml @@ -262,6 +262,10 @@ help: | /blocklist - Lista de seus usuários bloqueados /cancel - Cancela um pedido que não foi feito /cancelall - Cancela todos os pedidos postados e não atendidos + /scheduleorder <_buy|sell_> <_sats_> <_valor fiat_> <_código fiat_> <_método de pagamento_> [_prêmio_] - Cria um pedido recorrente que se republica automaticamente + /cancelschedule <_id do schedule_> - Para a republicação automática de um pedido programado + /listschedules - Lista seus pedidos programados ativos com seus IDs + /cancelallschedules - Cancela todos os seus pedidos programados /terms - Mostra os termos e condições ao usar o bot Nostr: @@ -724,3 +728,42 @@ community_enabled_by_admin: Um administrador reativou sua comunidade ${community community_already_disabled: Esta comunidade já está desativada community_already_enabled: Esta comunidade já está ativada ambiguous_community_input: Várias comunidades correspondem a esse nome de usuário, por favor use o ID da comunidade + +scheduleorder_usage: "Uso: /scheduleorder [prêmio]" +cancelschedule_usage: "Uso: /cancelschedule " +schedule_not_found: "⚠️ Schedule não encontrado ou já inativo." +schedule_cancelled: "✅ Schedule ${scheduleId} cancelado. Não serão mais publicadas ordens automaticamente." +listschedules_empty: "Você não tem ordens programadas ativas." +listschedules_header: "📋 Suas ordens programadas:" +schedule_list_item: | + 🆔 `${scheduleId}` + ${type} · ${fiatAmount} ${fiatCode} · ${paymentMethod} + 📅 ${days} · 🕐 ${hour} +cancelallschedules_none: "Você não tem ordens programadas ativas para cancelar." +all_schedules_cancelled: "✅ Foram canceladas ${count} ordem(ns) programada(s). Não serão mais publicadas ordens automaticamente." +schedule_req_max_reached: "⚠️ Você já tem ${max} ordens programadas ativas, que é o máximo permitido. Cancele uma com /cancelschedule antes de criar uma nova." +schedule_req_account_age: "⚠️ Sua conta deve ter pelo menos ${days} dias para criar ordens programadas." +schedule_req_completed_orders: "⚠️ Você precisa de pelo menos ${count} ordens concluídas para criar ordens programadas." +schedule_req_volume: "⚠️ Você precisa de um volume negociado de pelo menos ${volume} sats para criar ordens programadas." +schedule_req_rating: "⚠️ Você precisa de uma reputação de pelo menos ${rating} para criar ordens programadas." +schedule_req_completion_rate: "⚠️ Sua taxa de conclusão de ordens (${current}%) está abaixo dos ${rate}% exigidos. Conclua mais das suas ordens antes de programar." +schedule_dormant_removed: "⚠️ Removi todas as suas ordens programadas porque você não concluiu suas últimas ${count} ordens aceitas." +schedule_creation_cancelled: "❌ Criação do schedule cancelada." +schedule_created: "✅ Ordem programada criada! Seu ID é: ${scheduleId}\nUse /cancelschedule ${scheduleId} para pará-la." +schedule_choose_days: "📅 Em quais dias a ordem deve ser publicada?" +sched_daily: Todos os dias +sched_weekdays: Seg–Sex +sched_weekend: Fim de semana +sched_custom: Personalizado +schedule_enter_custom_days: "✍️ Insira os dias em inglês separados por vírgulas (ex: sunday, wednesday, friday)" +invalid_days: "⚠️ Dias inválidos. Insira os nomes dos dias em inglês separados por vírgulas (ex: sunday, wednesday, friday)." +schedule_enter_hour: "🕐 Em que hora a ordem deve ser publicada? Insira um número entre 0 e 23 (UTC)." +invalid_hour: "⚠️ Hora inválida. Por favor insira um número entre 0 e 23." +schedule_confirm_summary: | + 📋 *Resumo da ordem programada* + + Tipo: ${type} + Valor: ${fiatAmount} ${fiatCode} + Método de pagamento: ${paymentMethod} + Dias: ${days} + Hora: ${hour} diff --git a/locales/ru.yaml b/locales/ru.yaml index 15849091..fd7b4346 100644 --- a/locales/ru.yaml +++ b/locales/ru.yaml @@ -260,6 +260,10 @@ help: | /blocklist - Список заблокированных вами пользователей /cancel <_order id_> - Отменить заявку до ее исполнения /cancelall - Отменить все выставленные заявки + /scheduleorder <_buy|sell_> <_sats_> <_сумма фиат_> <_код фиат_> <_способ оплаты_> [_премиум_] - Создать повторяющуюся заявку с автопубликацией + /cancelschedule <_id расписания_> - Остановить автопубликацию запланированной заявки + /listschedules - Показывает ваши активные запланированные заявки с их ID + /cancelallschedules - Отменяет все ваши запланированные заявки /terms - показать условия использования бота Nostr: @@ -725,3 +729,42 @@ community_enabled_by_admin: Администратор повторно вклю community_already_disabled: Это сообщество уже отключено community_already_enabled: Это сообщество уже включено ambiguous_community_input: Несколько сообществ соответствуют этому имени пользователя, пожалуйста, используйте ID сообщества + +scheduleorder_usage: "Использование: /scheduleorder <сумма фиат> <код фиат> <способ оплаты> [премиум]" +cancelschedule_usage: "Использование: /cancelschedule " +schedule_not_found: "⚠️ Расписание не найдено или уже неактивно." +schedule_cancelled: "✅ Расписание ${scheduleId} отменено. Ордера больше не будут публиковаться автоматически." +listschedules_empty: "У вас нет активных запланированных заявок." +listschedules_header: "📋 Ваши запланированные заявки:" +schedule_list_item: | + 🆔 `${scheduleId}` + ${type} · ${fiatAmount} ${fiatCode} · ${paymentMethod} + 📅 ${days} · 🕐 ${hour} +cancelallschedules_none: "У вас нет активных запланированных заявок для отмены." +all_schedules_cancelled: "✅ Отменено запланированных заявок: ${count}. Ордера больше не будут публиковаться автоматически." +schedule_req_max_reached: "⚠️ У вас уже есть ${max} активных запланированных заявок — это максимум. Отмените одну командой /cancelschedule, прежде чем создавать новую." +schedule_req_account_age: "⚠️ Вашему аккаунту должно быть не менее ${days} дней, чтобы создавать запланированные заявки." +schedule_req_completed_orders: "⚠️ Вам нужно не менее ${count} завершённых ордеров, чтобы создавать запланированные заявки." +schedule_req_volume: "⚠️ Вам нужен торговый объём не менее ${volume} сатоши, чтобы создавать запланированные заявки." +schedule_req_rating: "⚠️ Вам нужна репутация не менее ${rating}, чтобы создавать запланированные заявки." +schedule_req_completion_rate: "⚠️ Ваш показатель завершения ордеров (${current}%) ниже требуемых ${rate}%. Завершите больше своих ордеров, прежде чем планировать." +schedule_dormant_removed: "⚠️ Я удалил все ваши запланированные заявки, потому что вы не завершили свои последние ${count} принятых ордеров." +schedule_creation_cancelled: "❌ Создание расписания отменено." +schedule_created: "✅ Плановый ордер создан! Ваш ID: ${scheduleId}\nИспользуйте /cancelschedule ${scheduleId} для остановки." +schedule_choose_days: "📅 В какие дни публиковать ордер?" +sched_daily: Каждый день +sched_weekdays: Пн–Пт +sched_weekend: Выходные +sched_custom: Настроить +schedule_enter_custom_days: "✍️ Введите дни на английском через запятую (например: sunday, wednesday, friday)" +invalid_days: "⚠️ Неверные дни. Введите названия дней на английском через запятую (например: sunday, wednesday, friday)." +schedule_enter_hour: "🕐 В какой час публиковать ордер? Введите число от 0 до 23 (UTC)." +invalid_hour: "⚠️ Неверный час. Введите число от 0 до 23." +schedule_confirm_summary: | + 📋 *Сводка планового ордера* + + Тип: ${type} + Сумма: ${fiatAmount} ${fiatCode} + Способ оплаты: ${paymentMethod} + Дни: ${days} + Час: ${hour} diff --git a/locales/uk.yaml b/locales/uk.yaml index bd9a5707..a29019fd 100644 --- a/locales/uk.yaml +++ b/locales/uk.yaml @@ -260,6 +260,10 @@ help: | /blocklist - Список ваших заблокованих користувачів /cancel <_order id_> - Скасувати заявку до її виконання /cancelall - Скасувати всі виставлені заявки + /scheduleorder <_buy|sell_> <_sats_> <_сума фіат_> <_код фіат_> <_спосіб оплати_> [_преміум_] - Створити повторювану заявку з автопублікацією + /cancelschedule <_id розкладу_> - Зупинити автопублікацію запланованої заявки + /listschedules - Показує ваші активні заплановані заявки з їх ID + /cancelallschedules - Скасовує всі ваші заплановані заявки /terms - Показати умови використання бота Nostr: @@ -721,3 +725,42 @@ community_enabled_by_admin: Адміністратор повторно увім community_already_disabled: Ця спільнота вже вимкнена community_already_enabled: Ця спільнота вже увімкнена ambiguous_community_input: Кілька спільнот відповідають цьому імені користувача, будь ласка, використовуйте ID спільноти + +scheduleorder_usage: "Використання: /scheduleorder <сума фіат> <код фіат> <спосіб оплати> [преміум]" +cancelschedule_usage: "Використання: /cancelschedule " +schedule_not_found: "⚠️ Розклад не знайдено або вже неактивний." +schedule_cancelled: "✅ Розклад ${scheduleId} скасовано. Ордери більше не будуть публікуватися автоматично." +listschedules_empty: "У вас немає активних запланованих заявок." +listschedules_header: "📋 Ваші заплановані заявки:" +schedule_list_item: | + 🆔 `${scheduleId}` + ${type} · ${fiatAmount} ${fiatCode} · ${paymentMethod} + 📅 ${days} · 🕐 ${hour} +cancelallschedules_none: "У вас немає активних запланованих заявок для скасування." +all_schedules_cancelled: "✅ Скасовано запланованих заявок: ${count}. Ордери більше не будуть публікуватися автоматично." +schedule_req_max_reached: "⚠️ У вас уже є ${max} активних запланованих заявок — це максимум. Скасуйте одну командою /cancelschedule, перш ніж створювати нову." +schedule_req_account_age: "⚠️ Вашому акаунту має бути щонайменше ${days} днів, щоб створювати заплановані заявки." +schedule_req_completed_orders: "⚠️ Вам потрібно щонайменше ${count} завершених ордерів, щоб створювати заплановані заявки." +schedule_req_volume: "⚠️ Вам потрібен торговий обсяг щонайменше ${volume} сатоші, щоб створювати заплановані заявки." +schedule_req_rating: "⚠️ Вам потрібна репутація щонайменше ${rating}, щоб створювати заплановані заявки." +schedule_req_completion_rate: "⚠️ Ваш показник завершення ордерів (${current}%) нижчий за необхідні ${rate}%. Завершіть більше своїх ордерів, перш ніж планувати." +schedule_dormant_removed: "⚠️ Я видалив усі ваші заплановані заявки, тому що ви не завершили свої останні ${count} прийнятих ордерів." +schedule_creation_cancelled: "❌ Створення розкладу скасовано." +schedule_created: "✅ Плановий ордер створено! Ваш ID: ${scheduleId}\nВикористовуйте /cancelschedule ${scheduleId} для зупинки." +schedule_choose_days: "📅 У які дні публікувати ордер?" +sched_daily: Щодня +sched_weekdays: Пн–Пт +sched_weekend: Вихідні +sched_custom: Налаштувати +schedule_enter_custom_days: "✍️ Введіть дні англійською через кому (наприклад: sunday, wednesday, friday)" +invalid_days: "⚠️ Невірні дні. Введіть назви днів англійською через кому (наприклад: sunday, wednesday, friday)." +schedule_enter_hour: "🕐 О якій годині публікувати ордер? Введіть число від 0 до 23 (UTC)." +invalid_hour: "⚠️ Недійсна година. Введіть число від 0 до 23." +schedule_confirm_summary: | + 📋 *Зведення планового ордера* + + Тип: ${type} + Сума: ${fiatAmount} ${fiatCode} + Спосіб оплати: ${paymentMethod} + Дні: ${days} + Година: ${hour} diff --git a/models/index.ts b/models/index.ts index f68837bd..2d697043 100644 --- a/models/index.ts +++ b/models/index.ts @@ -5,5 +5,15 @@ import Community from './community'; import Dispute from './dispute'; import Config from './config'; import Block from './block'; +import ScheduledOrder from './scheduled_order'; -export { User, Order, PendingPayment, Community, Dispute, Config, Block }; +export { + User, + Order, + PendingPayment, + Community, + Dispute, + Config, + Block, + ScheduledOrder, +}; diff --git a/models/scheduled_order.ts b/models/scheduled_order.ts new file mode 100644 index 00000000..94a6c62c --- /dev/null +++ b/models/scheduled_order.ts @@ -0,0 +1,44 @@ +import mongoose, { Document, Schema } from 'mongoose'; + +export interface IScheduledOrder extends Document { + creator_id: string; + type: string; + amount: number; + fiat_amount: number[]; + fiat_code: string; + payment_method: string; + price_margin: number; + community_id?: string; + // Weekdays (0=Sunday .. 6=Saturday, UTC) on which the order is published + days: number[]; + // Hour of the day (0-23, UTC) at which the order is published + hour: number; + // Remaining publication cycles before the schedule is exhausted + republish_count: number; + // The last order published from this schedule (mold -> order link) + last_order_id?: string | null; + active: boolean; + created_at: Date; +} + +const scheduledOrderSchema = new Schema({ + creator_id: { type: String, required: true }, + type: { type: String, required: true }, + amount: { type: Number, required: true }, + fiat_amount: { type: [Number], required: true }, + fiat_code: { type: String, required: true }, + payment_method: { type: String, required: true }, + price_margin: { type: Number, default: 0 }, + community_id: { type: String }, + days: { type: [Number], required: true }, + hour: { type: Number, required: true, min: 0, max: 23 }, + republish_count: { type: Number, default: 0 }, + last_order_id: { type: String, default: null }, + active: { type: Boolean, default: true }, + created_at: { type: Date, default: Date.now }, +}); + +export default mongoose.model( + 'ScheduledOrder', + scheduledOrderSchema, +); diff --git a/tests/bot/bot.spec.ts b/tests/bot/bot.spec.ts index b9877d98..7be30b6d 100644 --- a/tests/bot/bot.spec.ts +++ b/tests/bot/bot.spec.ts @@ -463,7 +463,7 @@ describe('Bot Initialization', () => { scheduledFunction(); - expect(scheduleStub.scheduleJob.callCount).to.be.equal(8); + expect(scheduleStub.scheduleJob.callCount).to.be.equal(9); expect(scheduleStub.scheduleJob.getCall(0).args[0]).to.equal( '*/10 * * * *', );