Mohammed Foud commited on
Commit
65d6a69
·
1 Parent(s): 14b96eb
src/bots/handlers/serviceHandlers.ts CHANGED
@@ -14,7 +14,9 @@ import {
14
  getNoSearchResultsMessage,
15
  getInvalidSearchInputMessage,
16
  getSearchCountryPromptMessage,
17
- getNoCountrySearchResultsMessage
 
 
18
  } from "../utils/messageUtils";
19
  import {
20
  getLoggedInMenuKeyboard,
@@ -202,6 +204,122 @@ export const setupServiceHandlers = (bot: any) => {
202
  }
203
  });
204
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  // Text handler for product/country search input
206
  bot.on('text', async (ctx: BotContext) => {
207
  try {
 
14
  getNoSearchResultsMessage,
15
  getInvalidSearchInputMessage,
16
  getSearchCountryPromptMessage,
17
+ getNoCountrySearchResultsMessage,
18
+ getNoAffordableProductsMessage,
19
+ getAffordableProductsMessage
20
  } from "../utils/messageUtils";
21
  import {
22
  getLoggedInMenuKeyboard,
 
204
  }
205
  });
206
 
207
+ // Add handler for 'buy_with_balance' button
208
+ bot.action('buy_with_balance', async (ctx: BotContext) => {
209
+ await ctx.answerCbQuery();
210
+ const telegramId = ctx.from?.id;
211
+ if (!telegramId || !authService.isUserLoggedIn(telegramId, ctx)) {
212
+ try {
213
+ await ctx.editMessageText(getAuthRequiredMessage(), { reply_markup: getMainMenuKeyboard().reply_markup });
214
+ } catch (editError: any) {
215
+ if (editError.message && editError.message.includes('message is not modified')) {
216
+ logger.info(`Auth required message not modified (buy_with_balance). No action needed.`);
217
+ } else {
218
+ logger.error(`Error editing auth required message (buy_with_balance): ${editError.message}`);
219
+ }
220
+ }
221
+ return;
222
+ }
223
+
224
+ try {
225
+ const user = await authService.getUserByTelegramId(telegramId, ctx);
226
+ if (!user) {
227
+ try {
228
+ await ctx.editMessageText(getAuthRequiredMessage(), { reply_markup: getMainMenuKeyboard().reply_markup });
229
+ } catch (editError: any) {
230
+ if (editError.message && editError.message.includes('message is not modified')) {
231
+ logger.info(`Auth required message not modified (buy_with_balance - user not found). No action needed.`);
232
+ } else {
233
+ logger.error(`Error editing auth required message (buy_with_balance - user not found): ${editError.message}`);
234
+ }
235
+ }
236
+ return;
237
+ }
238
+
239
+ const userBalance = user.balance;
240
+ const maxPrices = await virtualNumberService.getMaxPrices();
241
+
242
+ const affordableProducts = maxPrices.filter(product => {
243
+ // Convert product price to bot's currency (USD, assuming max-prices are in USD)
244
+ // And then apply profit calculation
245
+ const productPriceInBotCurrency = product.price;
246
+ const finalPrice = calculatePriceWithProfit(ctx, productPriceInBotCurrency, 'USD');
247
+ return userBalance >= finalPrice;
248
+ }).sort((a, b) => a.product.localeCompare(b.product)); // Sort alphabetically
249
+
250
+ if (affordableProducts.length === 0) {
251
+ try {
252
+ await ctx.editMessageText(getNoAffordableProductsMessage(userBalance, ctx), {
253
+ parse_mode: 'HTML',
254
+ reply_markup: getLoggedInMenuKeyboard().reply_markup
255
+ });
256
+ } catch (editError: any) {
257
+ if (editError.message && editError.message.includes('message is not modified')) {
258
+ logger.info(`No affordable products message not modified. No action needed.`);
259
+ } else {
260
+ logger.error(`Error editing no affordable products message: ${editError.message}`);
261
+ }
262
+ }
263
+ return;
264
+ }
265
+
266
+ const buttons = [];
267
+ const rowSize = 2;
268
+
269
+ for (let i = 0; i < affordableProducts.length; i += rowSize) {
270
+ const row = [];
271
+ for (let j = 0; j < rowSize && i + j < affordableProducts.length; j++) {
272
+ const product = affordableProducts[i + j];
273
+ const serviceData = fiveSimProducts[product.product];
274
+ if (serviceData) {
275
+ // Apply profit to the price for display
276
+ const displayPrice = calculatePriceWithProfit(ctx, product.price, 'USD');
277
+ row.push(
278
+ Markup.button.callback(
279
+ `${serviceData.icon} ${serviceData.label_en} (${formatPrice(ctx, displayPrice)})`,
280
+ `service_${product.product}`
281
+ )
282
+ );
283
+ } else {
284
+ logger.warn(`Service data not found for product: ${product.product}`);
285
+ }
286
+ }
287
+ if (row.length > 0) {
288
+ buttons.push(row);
289
+ }
290
+ }
291
+
292
+ buttons.push([Markup.button.callback('🔙 Back to Menu', 'logged_in_menu')]);
293
+
294
+ try {
295
+ await ctx.editMessageText(getAffordableProductsMessage(userBalance, ctx), {
296
+ parse_mode: 'HTML',
297
+ reply_markup: Markup.inlineKeyboard(buttons).reply_markup
298
+ });
299
+ } catch (editError: any) {
300
+ if (editError.message && editError.message.includes('message is not modified')) {
301
+ logger.info(`Affordable products message not modified. No action needed.`);
302
+ } else {
303
+ logger.error(`Error editing affordable products message: ${editError.message}`);
304
+ }
305
+ }
306
+
307
+ } catch (error: any) {
308
+ logger.error(`Error handling buy_with_balance: ${error.message}`);
309
+ try {
310
+ await ctx.editMessageText('⚠️ An error occurred while fetching affordable products. Please try again later.', {
311
+ reply_markup: getLoggedInMenuKeyboard().reply_markup
312
+ });
313
+ } catch (editError: any) {
314
+ if (editError.message && editError.message.includes('message is not modified')) {
315
+ logger.info(`Error message for affordable products not modified. No action needed.`);
316
+ } else {
317
+ logger.error(`Error editing error message for affordable products: ${editError.message}`);
318
+ }
319
+ }
320
+ }
321
+ });
322
+
323
  // Text handler for product/country search input
324
  bot.on('text', async (ctx: BotContext) => {
325
  try {
src/bots/services/VirtualNumberService.ts CHANGED
@@ -29,6 +29,13 @@ export interface ProductPrices {
29
  };
30
  }
31
 
 
 
 
 
 
 
 
32
  export class VirtualNumberService {
33
  private static instance: VirtualNumberService;
34
  private apiKey: string;
@@ -147,4 +154,23 @@ export class VirtualNumberService {
147
  throw new Error(`Failed to check SMS: ${error.message}`);
148
  }
149
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  }
 
29
  };
30
  }
31
 
32
+ export interface MaxPrice {
33
+ id: number;
34
+ product: string;
35
+ price: number;
36
+ CreatedAt: string; // date string
37
+ }
38
+
39
  export class VirtualNumberService {
40
  private static instance: VirtualNumberService;
41
  private apiKey: string;
 
154
  throw new Error(`Failed to check SMS: ${error.message}`);
155
  }
156
  }
157
+
158
+ /**
159
+ * Get a list of established price limits.
160
+ */
161
+ async getMaxPrices(): Promise<MaxPrice[]> {
162
+ try {
163
+ const response = await axios.get(`${this.baseUrl}/user/max-prices`, {
164
+ headers: {
165
+ 'Authorization': `Bearer ${this.apiKey}`,
166
+ 'Accept': 'application/json',
167
+ }
168
+ });
169
+
170
+ return response.data;
171
+ } catch (error: any) {
172
+ logger.error(`Error fetching max prices: ${error.message}`);
173
+ throw new Error(`Failed to fetch max prices: ${error.message}`);
174
+ }
175
+ }
176
  }
src/bots/utils/keyboardUtils.ts CHANGED
@@ -30,6 +30,7 @@ export const getLoggedInMenuKeyboard = () => {
30
  Markup.button.callback(messageManager.getMessage('btn_top_up'), 'top_up_balance'),
31
  Markup.button.callback(messageManager.getMessage('btn_history'), 'history')
32
  ],
 
33
  [
34
  Markup.button.callback(messageManager.getMessage('btn_back'), 'main_menu')
35
  ],
 
30
  Markup.button.callback(messageManager.getMessage('btn_top_up'), 'top_up_balance'),
31
  Markup.button.callback(messageManager.getMessage('btn_history'), 'history')
32
  ],
33
+ [Markup.button.callback('💰 Buy with Balance', 'buy_with_balance')],
34
  [
35
  Markup.button.callback(messageManager.getMessage('btn_back'), 'main_menu')
36
  ],
src/bots/utils/messageManager.ts CHANGED
@@ -90,6 +90,14 @@ class MessageManager {
90
  'no_country_search_results': {
91
  ar_value: 'عذراً، لم يتم العثور على دول مطابقة لبحثك.',
92
  en_value: 'Sorry, no matching countries found for your search.'
 
 
 
 
 
 
 
 
93
  }
94
  };
95
 
 
90
  'no_country_search_results': {
91
  ar_value: 'عذراً، لم يتم العثور على دول مطابقة لبحثك.',
92
  en_value: 'Sorry, no matching countries found for your search.'
93
+ },
94
+ 'affordable_products_list': {
95
+ ar_value: 'المنتجات التي يمكنك شراؤها برصيدك الحالي ({balance}):',
96
+ en_value: 'Products you can buy with your current balance ({balance}):'
97
+ },
98
+ 'no_affordable_products': {
99
+ ar_value: 'عذراً، لا توجد منتجات يمكنك شراؤها برصيدك الحالي ({balance}).',
100
+ en_value: 'Sorry, no products can be bought with your current balance ({balance}).'
101
  }
102
  };
103
 
src/bots/utils/messageUtils.ts CHANGED
@@ -177,4 +177,14 @@ export const getSearchCountryPromptMessage = () => {
177
 
178
  export const getNoCountrySearchResultsMessage = () => {
179
  return messageManager.getMessage('no_country_search_results');
 
 
 
 
 
 
 
 
 
 
180
  };
 
177
 
178
  export const getNoCountrySearchResultsMessage = () => {
179
  return messageManager.getMessage('no_country_search_results');
180
+ };
181
+
182
+ export const getAffordableProductsMessage = (balance: number, ctx: BotContext) => {
183
+ return messageManager.getMessage('affordable_products_list')
184
+ .replace('{balance}', formatPrice(ctx, balance));
185
+ };
186
+
187
+ export const getNoAffordableProductsMessage = (balance: number, ctx: BotContext) => {
188
+ return messageManager.getMessage('no_affordable_products')
189
+ .replace('{balance}', formatPrice(ctx, balance));
190
  };