Adobe-for-Alex / telegram-bot

0 stars 2 forks source link

Add interfaces and types #8

Closed gerushenka closed 1 month ago

gerushenka commented 1 month ago

Fixes #7

Chamber6821 commented 1 month ago

Покажи пример, как ты хочешь соединять свои абстракции

Вот что, я имею в виду

Chamber6821 commented 1 month ago

Пример как это может выглядеть

https://github.com/Adobe-for-Alex/adobe-api/pull/5#issuecomment-2446095897

gerushenka commented 1 month ago

Примерно так это вижу формируем сумму --> ищем админа --> запрашиваем верификацию --> назначаем сессию пользователю

async function processPayment(userId: TelegramId) {
    const payment = new ScreenPayment(userName, context.image());
    const price = await payment.price();
    const amount = await price.amount(); 

    const admin = await admins.anyNotBusy();
    if (!admin) {
        throw new Error("нет доступных админов");
    }

    const verification = await admin.requestPaymentVerification(payment);
    if (verification === 'approved') { 
        const user = await users.withId(userId);
        if (user) {
            const session = await service.createSession(userId, period);
            await user.setSubscription(session);
        }
    } else {
        console.log("REJECT");
    }
}

получается что оплата будет формироваться в price а amount будет итоговой суммой со скидкой

Chamber6821 commented 1 month ago

Это то, что нужно, опиши пожалуйста больше сценариев. Например отправка сообщений пользователю и приветствие нового. Каждая реакция бота - это какой-то объект

gerushenka commented 1 month ago

приветствие пользователя

регестрируем пользователя --> получаем имя и приветствуем --> предлагаем оформить подписку если её нет

async function newUser(userId: TelegramId, chatId: ChatId, nickname: TelegramNickname) {
    const users = await service.users();
    let user = await users.withId(userId);
    if (!user) {
        user = await users.register(userId, chatId);
    }

    const userName = await user.name();

    const welcomeMessage = `Привет, ${userName}! Добро пожаловать в наш сервис подписок.`;
    await sendTelegramMessage(chatId, welcomeMessage);

    const currentSubscription = await user.subscription();

    if (!currentSubscription) {
        const offerMessage = "У вас пока нет активной подписки. Хотите оформить её сейчас?";
        await sendTelegramMessage(chatId, offerMessage);
    }
}

напоминание о подписке

проверяем сколько дней до конца подписки и отправляем сообщение о скором истечении её


async function sendSubscriptionReminder(user: User, chatId: ChatId): Promise<void> {
    const session = await user.subscription();
    if (!session) {
        return; 
    }

    const endDate = await session.endDate();
    const currentDate = new Date();

    const daysRemaining = Math.ceil((endDate.getTime() - currentDate.getTime()) / (1000 * 60 * 60 * 24));
    if (daysRemaining <= 7) {
        const reminderMessage = `Ваша подписка заканчивается через ${daysRemaining} дней.'
        await sendTelegramMessage(chatId, reminderMessage);
    }
}

окончание подписки

вместо текста можно в целом использовать кнопки

async function handleSubscriptionEnd(userId: TelegramId) {
    const user = await users.withId(userId);
    const currentSession = await user.subscription();
    const hasExpired = !currentSession || new Date() > await currentSession.endDate();
    if (!hasExpired) {
        return;
    }

    const endMessage = "Ваша подписка закончилась. Хотите продлить её, чтобы сохранить доступ?";
    await sendTelegramMessage(userId, endMessage);

    const wantsToRenew = await waitForUserResponse(userId); 
    if (wantsToRenew.toLowerCase().includes("да")) {
        const period = 30; 
        const session = await service.createSession(userId, period);
        await user.setSubscription(session);

        const endDate = await session.endDate();
        const renewalMessage = `Подписка успешно продлена и будет действовать до ${endDate.toDateString()}. Спасибо, что остаетесь с нами!`;
        await sendTelegramMessage(userId, renewalMessage);
    } else {
        // Пользователь отказался от продления, предлагаем скидку
        const price = await getDiscountedPrice(userId);
        const discount = await price.discount();
        const discountedAmount = await price.amount();
        const discountMessage = `Продлите подписку сейчас и получите скидку ${discount}%! Всего за ${discountedAmount} рублей. Хотите воспользоваться предложением?`;
        await sendTelegramMessage(userId, discountMessage);

        const response = await waitForUserResponse(userId);
        if (response.toLowerCase().includes("да")) {
            const session = await service.createSession(userId, 30); // Подписка на 30 дней со скидкой
            await user.setSubscription(session);

            const endDate = await session.endDate();
            const finalMessage = `Спасибо за выбор нашего сервиса! Ваша подписка активирована до ${endDate.toDateString()}.`;
            await sendTelegramMessage(userId, finalMessage);
        } else {
            await sendTelegramMessage(userId, "Спасибо за использование нашего сервиса! Мы всегда рады вам.");
        }
    }
}
Chamber6821 commented 1 month ago

@gerushenka пиши код, пожалуйста, в таких кавычках: ```ts code ``` так он будет красиво подсвечиваться. ts - это TypeScript. Github поддерживает многие популярные языки

Chamber6821 commented 1 month ago

Это то, что я хотел (хотя и не в точности правильно в моем понимании)! Общая рекомендация: лучше не пладить кучу констант, а стараться писать все в одно выражение. Если становится сложно ориентироваться в этом выражении, это сигнал того, что стоит переформулировать его или разбить на отдельные самостоятельные части. Такой подход позволяет избегать портянок кода практически везде

Chamber6821 commented 1 month ago

По поводу этого сценария

Декомпозиция - в общем случае плохо, хотя и является самым простым с ходу решением Например, лучше вместо payment.price() сделать так, что бы price передавался в конструкторе и был инкапсулирован внутри payment. Пример:

const price = new DiscountPrice(new DollarPrice(42), 10)
user.sendMessage(new StringMessage(`Новая подписка обойдется вам в ${price.asString()}`))
const answer = user.answer()
admin.approve(
  userName
  new GeneralPayment(
    price
    answer.file()
  )
).extend(user.session())

admin.approve(payment: Payment): Promise<Payment> - заворачивает оплату в декоратор, что позволяет избежать ветвления в пользу полиморфизма

Chamber6821 commented 1 month ago

@gerushenka тебе понятна последняя пара рекомендация? Если да, то перепиши пожалуйста сценарии с учетом их. Еще, думаю лучше будет, если с пользователем ты будешь общаться через интерфейс User, что-то вроде:

Причем пускай message будет тоже интерфейсом, что бы мы могли прикреплять к сообщению картинки и файлы через декораторы: new MessageWithImage(new TextMessage('Привет, землянин!'), new Image('/assets/hello-cat.png'))

gerushenka commented 1 month ago

@gerushenka тебе понятна последняя пара рекомендация? Если да, то перепиши пожалуйста сценарии с учетом их. Еще, думаю лучше будет, если с пользователем ты будешь общаться через интерфейс User, что-то вроде:

  • user.send(message)
  • message = user.answer()

Причем пускай message будет тоже интерфейсом, что бы мы могли прикреплять к сообщению картинки и файлы через декораторы: new MessageWithImage(new TextMessage('Привет, землянин!'), new Image('/assets/hello-cat.png'))

Понял, сейчас сделаю

gerushenka commented 1 month ago

Оплата

async function processPayment(userId: TelegramId) {
    const price = new DiscountPrice(new DollarPrice(42), 10);
    user.send(new StringMessage(`Новая подписка обойдется вам в ${price.asString()}`))
    const admin = await admins.anyNotBusy();

    admin.approve(
    userName
    new GeneralPayment(
    price
    answer.file()
    )
  ).extend(user.session())
    const user = await users.withId(userId);

    if (verification === 'approved' && user) {
        const session = await service.createSession(userId, period);
        await user.setSubscription(session);
        await user.send(new TextMessage(`Оплата одобрена. Подписка активирована до ${session.endDate()}.`));
    } else {
        await user.send(new TextMessage("Извините, ваша оплата отклонена."));
    }
}

приветствие пользователя

регестрируем пользователя --> получаем имя и приветствуем --> предлагаем оформить подписку если её нет

async function newUser(userId: TelegramId, chatId: ChatId, nickname: TelegramNickname) {
    const users = await service.users();
    let user = await users.withId(userId);
    if (!user) {
        user = await users.register(userId, chatId);
    }

    const userName = await user.name();
    await user.send( new TextMessage(`Привет, ${userName}! Добро пожаловать в наш сервис.`));

    const currentSubscription = await user.subscription();

    if (!currentSubscription) {
        await user.send(new TextMessage("У вас пока нет активной подписки. Хотите оформить её сейчас?"));
    }
}

напоминание о подписке

проверяем сколько дней до конца подписки и отправляем сообщение о скором истечении её


async function sendSubscriptionReminder(user: User): Promise<void> {
    const session = await user.subscription();
    if (!session) {
        return; 
    }
    const endDate = await session.endDate();
    const currentDate = new Date();
    const daysRemaining = Math.ceil((endDate.getTime() - currentDate.getTime()) / (1000 * 60 * 60 * 24));
    if (daysRemaining <= 3) {
        await user.send(new TextMessage(`Ваша подписка заканчивается через ${daysRemaining} дней.`));
    }
}

окончание подписки

вместо текста можно в целом использовать кнопки

async function handleSubscriptionEnd(userId: TelegramId) {
    const user = await users.withId(userId);
    if (!user) return;

    const currentSession = await user.subscription();
    const hasExpired = !currentSession || new Date() > await currentSession.endDate();

    if (!hasExpired) {
        return;
    }

    await user.send(new TextMessage("Ваша подписка закончилась. Хотите продлить её, чтобы сохранить доступ?"));

    const wantsToRenew = await user.answer();
    if (wantsToRenew.asString().toLowerCase().includes("да")) {
        const period = 30;
        const session = await service.createSession(userId, period);
        await user.setSubscription(session);
        await user.send(new TextMessage(`Подписка успешно продлена и будет действовать до ${await session.endDate()}. Спасибо, что остаетесь с нами!`));
    } else {
        const discount = await price.discount();
        const discountedAmount = await price.discountedAmount();
        await user.send(new TextMessage(`Продлите подписку сейчас и получите скидку ${discount}%! Всего за ${discountedAmount} рублей. Хотите воспользоваться предложением?`));

        const response = await user.answer();
        if (response.asString().toLowerCase().includes("да")) {
            const session = await service.createSession(userId, 30);
            await user.setSubscription(session);

            await user.send(new TextMessage(`Ваша подписка активирована до ${await session.endDate()}.`));
        } else {
            await user.send(new TextMessage("Спасибо за использование нашего сервиса! Мы всегда рады вам."));
        }
    }
}
Chamber6821 commented 1 month ago

После изменений, раскидай все интерфейсы по файлам по шаблону: Payment -> src/payment/Payment.ts. Это нужно для группировки реализаций интерфейсов по папкам

Chamber6821 commented 1 month ago

В TS можно писать

export default interface A {}

вместо

interface A{}
export default A

Второй вариант менее предпочтителен из-за дублирования кода

Chamber6821 commented 1 month ago

Последние стилистические штрихи. Убери везде из объявления интерфейсов ;, но при этом оставь в строчках с import. Так же перемести все as* методы в конец объявленя интерфейса.

В будущем будет автоматический линтер, который сделает эту часть работы за тебя и мне не надо будет писать об этом. Оставить ; в строке import я сказал что бы было сейчас меньше гемороя, а стиль проекта выдерживался. Я так понял, что твой редактор (как и мой :)) автоматически добавляет импорт именно в таком формате