Compare commits

..

1 Commits

Author SHA1 Message Date
vchikalkin
72eab1dd0f feat: integrate Telegraf for Telegram notifications in order lifecycle
- Added Telegraf dependency to handle Telegram bot interactions.
- Updated order lifecycle methods to send notifications upon order creation and updates.
- Refactored datetime handling to use a consistent timezone.
- Removed unused utility functions to streamline codebase.
2025-07-22 15:44:23 +03:00
42 changed files with 582 additions and 789 deletions

View File

@ -9,25 +9,19 @@ jobs:
build-and-push:
name: Build and Push to Docker Hub
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.vars.outputs.tag }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set image tag
id: vars
run: echo "tag=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT
- name: Login to Docker Hub
run: echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
- name: Build image
run: docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/zapishis-strapi:${{ steps.vars.outputs.tag }} .
run: docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/zapishis-strapi:latest .
- name: Push image to Docker Hub
run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/zapishis-strapi:${{ steps.vars.outputs.tag }}
run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/zapishis-strapi:latest
deploy:
name: Deploy to VPS
@ -64,8 +58,6 @@ jobs:
echo "DATABASE_PASSWORD=${{ secrets.DATABASE_PASSWORD }}" >> .env
echo "DATABASE_SSL=false" >> .env
echo "BOT_TOKEN=${{ secrets.BOT_TOKEN }}" >> .env
echo "DOCKERHUB_USERNAME=${{ secrets.DOCKERHUB_USERNAME }}" >> .env
echo "STRAPI_IMAGE_TAG=${{ needs.build-and-push.outputs.tag }}" >> .env
- name: Copy .env to VPS via SCP
uses: appleboy/scp-action@master

4
.gitignore vendored
View File

@ -128,6 +128,4 @@ exports
dist
build
.strapi-updater.json
.strapi-cloud.json
*.cmd
.strapi-cloud.json

View File

@ -1,7 +1,7 @@
export default ({ env }) => ({
host: env('HOST', '0.0.0.0'),
port: env.int('PORT', 1337),
botToken: env('BOT_TOKEN', undefined),
bot_token: env('BOT_TOKEN'),
app: {
keys: env.array('APP_KEYS'),
},

View File

@ -1,28 +0,0 @@
services:
postgres:
image: postgres
ports:
- '127.0.0.1:5432:5432'
environment:
POSTGRES_USER: ${DATABASE_USERNAME}
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
POSTGRES_DB: ${DATABASE_NAME}
strapi:
build: .
ports:
- '127.0.0.1:1337:1337'
environment:
APP_KEYS: ${APP_KEYS}
API_TOKEN_SALT: ${API_TOKEN_SALT}
ADMIN_JWT_SECRET: ${ADMIN_JWT_SECRET}
TRANSFER_TOKEN_SALT: ${TRANSFER_TOKEN_SALT}
JWT_SECRET: ${JWT_SECRET}
DATABASE_HOST: ${DATABASE_HOST}
DATABASE_PORT: ${DATABASE_PORT}
DATABASE_NAME: ${DATABASE_NAME}
DATABASE_USERNAME: ${DATABASE_USERNAME}
DATABASE_PASSWORD: ${DATABASE_PASSWORD}
DATABASE_SSL: 'false'
BOT_TOKEN: ${BOT_TOKEN}

View File

@ -1,6 +1,6 @@
services:
strapi:
image: ${DOCKERHUB_USERNAME}/zapishis-strapi:${STRAPI_IMAGE_TAG}
image: vchikalkin/zapishis-strapi:latest
ports:
- "127.0.0.1:1337:1337"
environment:
@ -15,7 +15,6 @@ services:
DATABASE_USERNAME: ${DATABASE_USERNAME}
DATABASE_PASSWORD: ${DATABASE_PASSWORD}
DATABASE_SSL: "false"
BOT_TOKEN: ${BOT_TOKEN}
networks:
- app

View File

@ -0,0 +1,60 @@
{
"kind": "collectionType",
"collectionName": "blocks",
"info": {
"singularName": "block",
"pluralName": "blocks",
"displayName": "Block",
"description": ""
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {},
"attributes": {
"datetime_start": {
"type": "datetime",
"required": true
},
"datetime_end": {
"type": "datetime",
"required": true
},
"state": {
"type": "enumeration",
"enum": [
"created",
"paid",
"deleted"
],
"default": "created"
},
"sessions_total": {
"type": "integer",
"default": 10,
"required": true
},
"sessions_completed": {
"type": "integer",
"default": 0
},
"master": {
"type": "relation",
"relation": "manyToOne",
"target": "api::customer.customer",
"inversedBy": "blocks"
},
"client": {
"type": "relation",
"relation": "manyToOne",
"target": "api::customer.customer",
"inversedBy": "blocks"
},
"orders": {
"type": "relation",
"relation": "oneToMany",
"target": "api::order.order",
"mappedBy": "block"
}
}
}

View File

@ -0,0 +1,7 @@
/**
* block controller
*/
import { factories } from '@strapi/strapi'
export default factories.createCoreController('api::block.block');

View File

@ -0,0 +1,7 @@
/**
* block router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::block.block');

View File

@ -0,0 +1,7 @@
/**
* block service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::block.block');

View File

@ -1,24 +0,0 @@
{
"kind": "collectionType",
"collectionName": "customer_settings",
"info": {
"singularName": "customer-setting",
"pluralName": "customer-settings",
"displayName": "CustomerSettings"
},
"options": {
"draftAndPublish": false
},
"pluginOptions": {},
"attributes": {
"autoRenewSubscription": {
"type": "boolean"
},
"customer": {
"type": "relation",
"relation": "oneToOne",
"target": "api::customer.customer",
"mappedBy": "customer_setting"
}
}
}

View File

@ -14,48 +14,47 @@
"attributes": {
"name": {
"type": "string",
"maxLength": 100,
"required": true
},
"surname": {
"type": "string",
"maxLength": 100,
"required": false
},
"telegramId": {
"type": "biginteger",
"unique": true
},
"phone": {
"type": "string",
"maxLength": 20,
"required": true,
"unique": true
"unique": true,
"required": true
},
"role": {
"type": "enumeration",
"required": true,
"enum": [
"client",
"master"
]
],
"required": true
},
"active": {
"type": "boolean",
"required": false,
"default": false
"default": false,
"required": false
},
"invited": {
"clients": {
"type": "relation",
"relation": "manyToMany",
"target": "api::customer.customer",
"inversedBy": "invitedBy"
"inversedBy": "masters"
},
"invitedBy": {
"masters": {
"type": "relation",
"relation": "manyToMany",
"target": "api::customer.customer",
"mappedBy": "invited"
"mappedBy": "clients"
},
"blocks": {
"type": "relation",
"relation": "oneToMany",
"target": "api::block.block",
"mappedBy": "client"
},
"slots": {
"type": "relation",
@ -77,27 +76,6 @@
"relation": "oneToMany",
"target": "api::service.service",
"mappedBy": "master"
},
"subscriptions": {
"type": "relation",
"relation": "oneToMany",
"target": "api::subscription.subscription",
"mappedBy": "customer"
},
"bannedUntil": {
"type": "datetime"
},
"subscription_rewards": {
"type": "relation",
"relation": "oneToMany",
"target": "api::subscription-reward.subscription-reward",
"mappedBy": "owner"
},
"customer_setting": {
"type": "relation",
"relation": "oneToOne",
"target": "api::customer.customer-setting",
"inversedBy": "customer"
}
}
}

View File

@ -1,6 +1,23 @@
import { bot, extractId, dayjs } from '../../../../utils';
import { DEFAULT_TZ } from '../../../../constants';
const ERR_MISSING_TIME = 'Не указано время';
const ERR_INVALID_TIME = 'Некорректное время';
const ERR_OVERLAPPING_TIME = 'Время пересекается с другими заказами';
const ERR_INACTIVE_CLIENT = 'Клиент не активен';
const ERR_INACTIVE_MASTER = 'Мастер не активен';
const ERR_SLOT_CLOSED = 'Слот закрыт';
const ERR_INVALID_CLIENT = 'Некорректный клиент';
const ERR_INVALID_MASTER = 'Некорректный мастер';
const ERR_MISSING_CLIENT = 'Не указан клиент';
const ERR_MISSING_SLOT = 'Не указан слот';
const ERR_MISSING_SERVICE = 'Не указан сервис';
const ERR_ORDER_OUT_OF_SLOT = 'Время заказа выходит за пределы слота';
const ERR_EXISTING_ORDER_OR_SLOT_NOT_FOUND =
'Существующий заказ или слот не найден';
const ERR_CANNOT_COMPLETE_BEFORE_START =
'Нельзя завершить запись до её наступления';
const STATE_MAP: Record<State, string> = {
approved: 'Подтверждено',
cancelled: 'Отменено',
@ -69,20 +86,13 @@ async function sendTelegramNotification(orderEntity: Order, isUpdate = false) {
const emojiForState = EMOJI_MAP[state] || '';
const stateLabel = STATE_MAP[state] || state;
// Эмодзи в заголовке: карандаш при обновлении, флаг для завершенных, иначе эмодзи статуса
const headingEmoji = isUpdate
? (state === 'completed' ? '🏁' : '✏️')
: emojiForState;
// Эмодзи в заголовке: карандаш при обновлении, иначе эмодзи статуса
const headingEmoji = isUpdate ? '✏️' : emojiForState;
let heading = '';
if (isUpdate) {
// Специальная обработка для завершенных записей
if (state === 'completed') {
heading = `${headingEmoji} <b>Запись завершена</b>`;
} else {
heading = `${headingEmoji} <b>Запись изменена</b>`;
}
heading = `${headingEmoji} <b>Запись изменена</b>`;
} else {
const isApproved = state === 'approved';
const creationText = isApproved
@ -106,37 +116,20 @@ async function sendTelegramNotification(orderEntity: Order, isUpdate = false) {
const clientName = order.client?.name || '-';
const masterName = slot.master?.name || '-';
// Формируем список всех услуг и вычисляем общую стоимость
let servicesList = '-';
let totalPrice = 0;
if (order.services?.length) {
servicesList = order.services.map(service => service.name).join(', ');
// Вычисляем общую стоимость
totalPrice = order.services.reduce((sum, service) => {
return sum + (service.price || 0);
}, 0);
}
// Форматируем цену для отображения
const priceText = totalPrice > 0 ? `${totalPrice.toLocaleString('ru-RU')}` : 'Не указана';
const serviceName = order.services?.[0]?.name || '-';
const messageForMaster = `${heading}
<b>Дата:</b> ${date}
<b>Время:</b> ${timeStartString} - ${timeEndString}
<b>Клиент:</b> ${clientName}
<b>Услуги:</b> ${servicesList}
<b>Стоимость:</b> ${priceText}
<b>Услуга:</b> ${serviceName}
<b>Статус:</b> ${emojiForState} ${stateLabel}`;
const messageForClient = `${heading}
<b>Дата:</b> ${date}
<b>Время:</b> ${timeStartString} - ${timeEndString}
<b>Мастер:</b> ${masterName}
<b>Услуги:</b> ${servicesList}
<b>Стоимость:</b> ${priceText}
<b>Услуга:</b> ${serviceName}
<b>Статус:</b> ${emojiForState} ${stateLabel}`;
if (masterTelegramId) {
@ -176,4 +169,189 @@ export default {
await sendTelegramNotification(updatedEntity, true);
},
async beforeCreate(event) {
const { data } = event.params;
const { datetime_start, datetime_end, client, services } = data;
const clientId = extractId(client);
const slotId = extractId(data.slot);
// Проверка наличия обязательных полей
if (!slotId) throw new Error(ERR_MISSING_SLOT);
if (!clientId) throw new Error(ERR_MISSING_CLIENT);
if (!extractId(services)) throw new Error(ERR_MISSING_SERVICE);
// Проверка корректности времени заказа.
if (!datetime_start || !datetime_end) {
throw new Error(ERR_MISSING_TIME);
}
if (new Date(datetime_end) <= new Date(datetime_start)) {
throw new Error(ERR_INVALID_TIME);
}
// Получаем слот
const slot = await strapi.db.query('api::slot.slot').findOne({
where: { id: slotId },
populate: ['master'],
});
if (!slot) throw new Error(ERR_MISSING_SLOT);
// Проверка, что заказ укладывается в рамки слота
if (
new Date(datetime_start) < new Date(slot.datetime_start) ||
new Date(datetime_end) > new Date(slot.datetime_end)
) {
throw new Error(ERR_ORDER_OUT_OF_SLOT);
}
// 1. Слот не должен быть закрыт
if (slot.state === 'closed') {
throw new Error(ERR_SLOT_CLOSED);
}
// Получаем клиента
const clientEntity = await strapi.db
.query('api::customer.customer')
.findOne({
where: { id: clientId },
populate: { masters: true },
});
if (!clientEntity) throw new Error(ERR_MISSING_CLIENT);
// Проверка активности клиента
if (!clientEntity.active) {
throw new Error(ERR_INACTIVE_CLIENT);
}
// Получаем мастера слота
const slotMaster = slot.master;
if (!slotMaster) throw new Error(ERR_INVALID_MASTER);
if (!slotMaster.active || slotMaster.role !== 'master') {
throw new Error(ERR_INACTIVE_MASTER);
}
// 2. Проверка ролей и связей
const isClientMaster = clientEntity.role === 'master';
const slotMasterId = slotMaster.id;
if (!isClientMaster) {
// Клиент не должен быть мастером слота
if (clientEntity.id === slotMasterId) {
throw new Error(ERR_INVALID_CLIENT);
}
// Клиент должен быть в списке клиентов мастера
const masters = clientEntity.masters?.map(m => m.id) || [];
if (!masters.includes(slotMasterId)) {
throw new Error(ERR_INVALID_MASTER);
}
} else {
// Мастер не может записать другого мастера
if (slotMasterId !== clientEntity.id) {
throw new Error(ERR_INVALID_MASTER);
}
}
// Проверка пересечений заказов по времени.
const overlappingEntities = await strapi.db
.query('api::order.order')
.findMany({
where: {
documentId: { $ne: data.documentId },
datetime_start: { $lt: datetime_end },
datetime_end: { $gt: datetime_start },
slot: {
id: { $eq: slotId },
},
state: {
$notIn: ['cancelled'],
},
},
populate: ['slot'],
});
if (overlappingEntities.length > 0) {
throw new Error(ERR_OVERLAPPING_TIME);
}
},
async beforeUpdate(event) {
const { data, where } = event.params;
const { id: entityId } = where;
const { datetime_start, datetime_end, state } = data;
const existingOrder = await strapi.db.query('api::order.order').findOne({
where: { id: entityId },
select: ['documentId', 'datetime_start', 'datetime_end'],
populate: ['slot', 'client'],
});
if (state && !datetime_start && !datetime_end) {
if (state === 'completed') {
const clientId = extractId(existingOrder.client);
const lastOrder = await strapi.db.query('api::order.order').findMany({
where: {
client: {
id: clientId,
},
state: 'completed',
},
orderBy: { order_number: 'desc' },
limit: 1,
});
const lastOrderNumber =
lastOrder.length > 0 ? lastOrder[0].order_number : 0;
data.order_number = lastOrderNumber + 1;
}
const now = dayjs().tz(DEFAULT_TZ);
const orderStart = dayjs(existingOrder.datetime_start).tz(DEFAULT_TZ);
if (state === 'completed' && now.isBefore(orderStart, 'minute')) {
throw new Error(ERR_CANNOT_COMPLETE_BEFORE_START);
}
return;
}
if (
existingOrder.datetime_start === datetime_start &&
existingOrder.datetime_end === datetime_end
) {
return;
}
if (!datetime_start || !datetime_end) {
throw new Error(ERR_INVALID_TIME);
}
if (new Date(datetime_end) <= new Date(datetime_start)) {
throw new Error(ERR_INVALID_TIME);
}
if (!existingOrder || !existingOrder.slot) {
throw new Error(ERR_EXISTING_ORDER_OR_SLOT_NOT_FOUND);
}
const slotId = existingOrder.slot.documentId;
const overlappingEntities = await strapi.db
.query('api::order.order')
.findMany({
where: {
id: { $ne: entityId },
datetime_start: { $lt: datetime_end },
datetime_end: { $gt: datetime_start },
slot: {
documentId: { $eq: slotId },
},
},
populate: ['slot'],
});
if (overlappingEntities.length > 0) {
throw new Error(ERR_OVERLAPPING_TIME);
}
},
};

View File

@ -30,6 +30,12 @@
"target": "api::customer.customer",
"inversedBy": "orders"
},
"block": {
"type": "relation",
"relation": "manyToOne",
"target": "api::block.block",
"inversedBy": "orders"
},
"order_number": {
"type": "integer"
},

View File

@ -4,7 +4,7 @@
"info": {
"singularName": "service",
"pluralName": "services",
"displayName": "Service",
"displayName": "service",
"description": ""
},
"options": {
@ -13,9 +13,7 @@
"pluginOptions": {},
"attributes": {
"name": {
"type": "string",
"maxLength": 100,
"required": true
"type": "string"
},
"orders": {
"type": "relation",
@ -33,17 +31,6 @@
"type": "time",
"required": true,
"default": "01:00:00.000"
},
"active": {
"type": "boolean",
"default": false
},
"price": {
"type": "decimal",
"min": 1
},
"description": {
"type": "text"
}
}
}

View File

@ -0,0 +1,20 @@
{
"kind": "collectionType",
"collectionName": "settings",
"info": {
"singularName": "setting",
"pluralName": "settings",
"displayName": "Setting",
"description": ""
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {},
"attributes": {
"recording_by_blocks": {
"type": "boolean",
"default": false
}
}
}

View File

@ -0,0 +1,7 @@
/**
* setting controller
*/
import { factories } from '@strapi/strapi'
export default factories.createCoreController('api::setting.setting');

View File

@ -0,0 +1,7 @@
/**
* setting router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::setting.setting');

View File

@ -0,0 +1,7 @@
/**
* setting service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::setting.setting');

View File

@ -0,0 +1,144 @@
import { DEFAULT_TZ } from '../../../../constants';
import { extractId, dayjs } from '../../../../utils';
const ERR_INVALID_TIME = 'Некорректное время';
const ERR_OVERLAPPING_TIME = 'Время пересекается с другими слотами';
const ERR_FORBIDDEN_SLOT_STATUS =
'Нельзя менять время слота, если есть связанные заказы';
const FORBIDDEN_ORDER_STATES = [
'scheduled',
'approved',
'completed',
'cancelling',
];
const ERR_INACTIVE_MASTER = 'Мастер не активен';
const ERR_INVALID_MASTER = 'Некорректный мастер';
const ERR_PAST_SLOT = 'Нельзя создать слот в прошлом';
const ERR_SLOT_HAS_ORDERS = 'Нельзя удалить слот с активными заказами';
const ERR_RECORD_NOT_FOUND = 'Запись не найдена';
export default {
async beforeCreate(event) {
const { data } = event.params;
const { master, datetime_start, datetime_end } = data;
// Проверка, что мастер существует и активен
const masterId = extractId(master);
const masterEntity = await strapi.db
.query('api::customer.customer')
.findOne({
where: { id: masterId },
});
if (!masterEntity) throw new Error(ERR_INVALID_MASTER);
if (!masterEntity.active || masterEntity.role !== 'master') {
throw new Error(ERR_INACTIVE_MASTER);
}
// Проверка, что слот не создаётся в прошлом
if (datetime_start) {
const now = dayjs().tz(DEFAULT_TZ);
const slotStart = dayjs(datetime_start).tz(DEFAULT_TZ);
if (slotStart.isBefore(now, 'minute')) {
throw new Error(ERR_PAST_SLOT);
}
}
// Проверка валидности времени
if (!datetime_start || !datetime_end) {
throw new Error(ERR_INVALID_TIME);
}
if (new Date(datetime_end) <= new Date(datetime_start)) {
throw new Error(ERR_INVALID_TIME);
}
const overlappingEntities = await strapi.db
.query('api::slot.slot')
.findMany({
where: {
documentId: { $ne: data.documentId },
datetime_start: { $lt: datetime_end },
datetime_end: { $gt: datetime_start },
master: masterId,
},
});
if (overlappingEntities.length > 0) {
throw new Error(ERR_OVERLAPPING_TIME);
}
},
async beforeUpdate(event) {
const { data, where } = event.params;
const { id: entityId } = where;
// Если меняется хотя бы одно из полей времени
const isTimeChange = 'datetime_start' in data || 'datetime_end' in data;
if (isTimeChange) {
let datetime_start = data.datetime_start;
let datetime_end = data.datetime_end;
// Подтянуть недостающие значения из существующего слота
const existingSlot = await strapi.db.query('api::slot.slot').findOne({
where: { id: entityId },
select: ['datetime_start', 'datetime_end'],
});
if (!datetime_start) datetime_start = existingSlot?.datetime_start;
if (!datetime_end) datetime_end = existingSlot?.datetime_end;
// Проверка: оба времени должны быть определены
if (!datetime_start || !datetime_end) {
throw new Error(ERR_INVALID_TIME);
}
// Проверка валидности времени
if (new Date(datetime_end) <= new Date(datetime_start)) {
throw new Error(ERR_INVALID_TIME);
}
const existingEntity = await strapi.db.query('api::slot.slot').findOne({
where: { id: entityId },
select: ['documentId'],
populate: ['orders'],
});
const orders = existingEntity?.orders;
if (
orders?.length > 0 &&
orders?.some(order => FORBIDDEN_ORDER_STATES.includes(order.state))
) {
throw new Error(ERR_FORBIDDEN_SLOT_STATUS);
}
if (!existingEntity) {
throw new Error(ERR_RECORD_NOT_FOUND);
}
const { documentId } = existingEntity;
const overlappingEntities = await strapi.db
.query('api::slot.slot')
.findMany({
where: {
id: { $ne: entityId },
documentId: { $ne: documentId },
datetime_start: { $lt: datetime_end },
datetime_end: { $gt: datetime_start },
},
});
if (overlappingEntities.length > 0) {
throw new Error(ERR_OVERLAPPING_TIME);
}
}
},
async beforeDelete(event) {
const { where } = event.params;
const slotId = where.id;
const slot = await strapi.db.query('api::slot.slot').findOne({
where: { id: slotId },
populate: ['orders'],
});
if (slot?.orders?.length) {
throw new Error(ERR_SLOT_HAS_ORDERS);
}
},
};

View File

@ -1,67 +0,0 @@
{
"kind": "collectionType",
"collectionName": "subscription_histories",
"info": {
"singularName": "subscription-history",
"pluralName": "subscription-histories",
"displayName": "SubscriptionHistory"
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {},
"attributes": {
"subscription": {
"type": "relation",
"relation": "oneToOne",
"target": "api::subscription.subscription",
"mappedBy": "subscription_history"
},
"subscription_price": {
"type": "relation",
"relation": "oneToOne",
"target": "api::subscription-price.subscription-price"
},
"amount": {
"type": "decimal",
"required": true
},
"currency": {
"type": "string",
"default": "RUB",
"required": false
},
"state": {
"type": "enumeration",
"required": true,
"enum": [
"success",
"failed",
"pending"
]
},
"paymentId": {
"type": "string",
"required": false
},
"source": {
"type": "enumeration",
"required": true,
"default": "payment",
"enum": [
"payment",
"trial",
"reward",
"admin",
"renewal"
]
},
"description": {
"type": "text"
},
"period": {
"type": "string",
"maxLength": 20
}
}
}

View File

@ -1,7 +0,0 @@
/**
* subscription-history controller
*/
import { factories } from '@strapi/strapi'
export default factories.createCoreController('api::subscription-history.subscription-history');

View File

@ -1,7 +0,0 @@
/**
* subscription-history router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::subscription-history.subscription-history');

View File

@ -1,7 +0,0 @@
/**
* subscription-history service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::subscription-history.subscription-history');

View File

@ -1,48 +0,0 @@
{
"kind": "collectionType",
"collectionName": "subscription_prices",
"info": {
"singularName": "subscription-price",
"pluralName": "subscription-prices",
"displayName": "SubscriptionPrice"
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {},
"attributes": {
"period": {
"type": "enumeration",
"required": true,
"enum": [
"trial",
"day",
"week",
"month",
"half_year",
"year"
]
},
"amount": {
"type": "decimal",
"required": true
},
"currency": {
"type": "string",
"default": "RUB",
"required": false
},
"active": {
"type": "boolean",
"required": false,
"default": true
},
"description": {
"type": "text"
},
"days": {
"type": "integer",
"required": true
}
}
}

View File

@ -1,7 +0,0 @@
/**
* subscription-price controller
*/
import { factories } from '@strapi/strapi'
export default factories.createCoreController('api::subscription-price.subscription-price');

View File

@ -1,7 +0,0 @@
/**
* subscription-price router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::subscription-price.subscription-price');

View File

@ -1,7 +0,0 @@
/**
* subscription-price service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::subscription-price.subscription-price');

View File

@ -1,47 +0,0 @@
{
"kind": "collectionType",
"collectionName": "subscription_rewards",
"info": {
"singularName": "subscription-reward",
"pluralName": "subscription-rewards",
"displayName": "SubscriptionReward"
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {},
"attributes": {
"activated": {
"type": "boolean",
"default": false
},
"owner": {
"type": "relation",
"relation": "manyToOne",
"target": "api::customer.customer",
"inversedBy": "subscription_rewards"
},
"invited": {
"type": "relation",
"relation": "oneToOne",
"target": "api::customer.customer"
},
"days": {
"type": "integer",
"required": true
},
"expiresAt": {
"type": "datetime",
"required": true
},
"description": {
"type": "text"
},
"subscription": {
"type": "relation",
"relation": "manyToOne",
"target": "api::subscription.subscription",
"inversedBy": "subscription_rewards"
}
}
}

View File

@ -1,7 +0,0 @@
/**
* subscription-reward controller
*/
import { factories } from '@strapi/strapi'
export default factories.createCoreController('api::subscription-reward.subscription-reward');

View File

@ -1,7 +0,0 @@
/**
* subscription-reward router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::subscription-reward.subscription-reward');

View File

@ -1,7 +0,0 @@
/**
* subscription-reward service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::subscription-reward.subscription-reward');

View File

@ -1,29 +0,0 @@
{
"kind": "singleType",
"collectionName": "subscription_settings",
"info": {
"singularName": "subscription-setting",
"pluralName": "subscription-settings",
"displayName": "SubscriptionSettings"
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {},
"attributes": {
"maxOrdersPerMonth": {
"type": "integer",
"required": true,
"default": 20
},
"referralRewardDays": {
"type": "integer",
"required": true,
"default": 1
},
"proEnabled": {
"type": "boolean",
"default": false
}
}
}

View File

@ -1,7 +0,0 @@
/**
* subscription-setting controller
*/
import { factories } from '@strapi/strapi'
export default factories.createCoreController('api::subscription-setting.subscription-setting');

View File

@ -1,7 +0,0 @@
/**
* subscription-setting router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::subscription-setting.subscription-setting');

View File

@ -1,7 +0,0 @@
/**
* subscription-setting service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::subscription-setting.subscription-setting');

View File

@ -1,47 +0,0 @@
{
"kind": "collectionType",
"collectionName": "subscriptions",
"info": {
"singularName": "subscription",
"pluralName": "subscriptions",
"displayName": "Subscription"
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {},
"attributes": {
"customer": {
"type": "relation",
"relation": "manyToOne",
"target": "api::customer.customer",
"inversedBy": "subscriptions"
},
"active": {
"type": "boolean",
"required": true,
"default": false
},
"expiresAt": {
"type": "datetime",
"required": true
},
"subscription_history": {
"type": "relation",
"relation": "oneToOne",
"target": "api::subscription-history.subscription-history",
"inversedBy": "subscription"
},
"subscription_rewards": {
"type": "relation",
"relation": "oneToMany",
"target": "api::subscription-reward.subscription-reward",
"mappedBy": "subscription"
},
"nextSubscription": {
"type": "relation",
"relation": "oneToOne",
"target": "api::subscription.subscription"
}
}
}

View File

@ -1,7 +0,0 @@
/**
* subscription controller
*/
import { factories } from '@strapi/strapi'
export default factories.createCoreController('api::subscription.subscription');

View File

@ -1,7 +0,0 @@
/**
* subscription router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::subscription.subscription');

View File

@ -1,7 +0,0 @@
/**
* subscription service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::subscription.subscription');

View File

@ -9,8 +9,5 @@ export function extractId(input) {
if (input?.set?.[0]?.id) {
return input.set[0].id;
}
if (input?.connect?.[0]?.id) {
return input.connect[0].id;
}
return null;
}

View File

@ -1,5 +1,3 @@
import { Telegraf } from 'telegraf';
const botToken = strapi.config.get('server.botToken') as string;
export const bot = new Telegraf(botToken);
export const bot = new Telegraf(process.env.bot_token);

View File

@ -373,6 +373,43 @@ export interface AdminUser extends Struct.CollectionTypeSchema {
};
}
export interface ApiBlockBlock extends Struct.CollectionTypeSchema {
collectionName: 'blocks';
info: {
description: '';
displayName: 'Block';
pluralName: 'blocks';
singularName: 'block';
};
options: {
draftAndPublish: true;
};
attributes: {
client: Schema.Attribute.Relation<'manyToOne', 'api::customer.customer'>;
createdAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
datetime_end: Schema.Attribute.DateTime & Schema.Attribute.Required;
datetime_start: Schema.Attribute.DateTime & Schema.Attribute.Required;
locale: Schema.Attribute.String & Schema.Attribute.Private;
localizations: Schema.Attribute.Relation<'oneToMany', 'api::block.block'> &
Schema.Attribute.Private;
master: Schema.Attribute.Relation<'manyToOne', 'api::customer.customer'>;
orders: Schema.Attribute.Relation<'oneToMany', 'api::order.order'>;
publishedAt: Schema.Attribute.DateTime;
sessions_completed: Schema.Attribute.Integer &
Schema.Attribute.DefaultTo<0>;
sessions_total: Schema.Attribute.Integer &
Schema.Attribute.Required &
Schema.Attribute.DefaultTo<10>;
state: Schema.Attribute.Enumeration<['created', 'paid', 'deleted']> &
Schema.Attribute.DefaultTo<'created'>;
updatedAt: Schema.Attribute.DateTime;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
};
}
export interface ApiCustomerCustomer extends Struct.CollectionTypeSchema {
collectionName: 'customers';
info: {
@ -386,55 +423,29 @@ export interface ApiCustomerCustomer extends Struct.CollectionTypeSchema {
};
attributes: {
active: Schema.Attribute.Boolean & Schema.Attribute.DefaultTo<false>;
bannedUntil: Schema.Attribute.DateTime;
blocks: Schema.Attribute.Relation<'oneToMany', 'api::block.block'>;
clients: Schema.Attribute.Relation<'manyToMany', 'api::customer.customer'>;
createdAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
customer_setting: Schema.Attribute.Relation<
'oneToOne',
'api::customer.customer-setting'
>;
invited: Schema.Attribute.Relation<'manyToMany', 'api::customer.customer'>;
invitedBy: Schema.Attribute.Relation<
'manyToMany',
'api::customer.customer'
>;
locale: Schema.Attribute.String & Schema.Attribute.Private;
localizations: Schema.Attribute.Relation<
'oneToMany',
'api::customer.customer'
> &
Schema.Attribute.Private;
name: Schema.Attribute.String &
Schema.Attribute.Required &
Schema.Attribute.SetMinMaxLength<{
maxLength: 100;
}>;
masters: Schema.Attribute.Relation<'manyToMany', 'api::customer.customer'>;
name: Schema.Attribute.String & Schema.Attribute.Required;
orders: Schema.Attribute.Relation<'oneToMany', 'api::order.order'>;
phone: Schema.Attribute.String &
Schema.Attribute.Required &
Schema.Attribute.Unique &
Schema.Attribute.SetMinMaxLength<{
maxLength: 20;
}>;
Schema.Attribute.Unique;
photoUrl: Schema.Attribute.String;
publishedAt: Schema.Attribute.DateTime;
role: Schema.Attribute.Enumeration<['client', 'master']> &
Schema.Attribute.Required;
services: Schema.Attribute.Relation<'oneToMany', 'api::service.service'>;
slots: Schema.Attribute.Relation<'oneToMany', 'api::slot.slot'>;
subscription_rewards: Schema.Attribute.Relation<
'oneToMany',
'api::subscription-reward.subscription-reward'
>;
subscriptions: Schema.Attribute.Relation<
'oneToMany',
'api::subscription.subscription'
>;
surname: Schema.Attribute.String &
Schema.Attribute.SetMinMaxLength<{
maxLength: 100;
}>;
telegramId: Schema.Attribute.BigInteger & Schema.Attribute.Unique;
updatedAt: Schema.Attribute.DateTime;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
@ -442,36 +453,6 @@ export interface ApiCustomerCustomer extends Struct.CollectionTypeSchema {
};
}
export interface ApiCustomerCustomerSetting
extends Struct.CollectionTypeSchema {
collectionName: 'customer_settings';
info: {
displayName: 'CustomerSettings';
pluralName: 'customer-settings';
singularName: 'customer-setting';
};
options: {
draftAndPublish: false;
};
attributes: {
autoRenewSubscription: Schema.Attribute.Boolean;
createdAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
customer: Schema.Attribute.Relation<'oneToOne', 'api::customer.customer'>;
locale: Schema.Attribute.String & Schema.Attribute.Private;
localizations: Schema.Attribute.Relation<
'oneToMany',
'api::customer.customer-setting'
> &
Schema.Attribute.Private;
publishedAt: Schema.Attribute.DateTime;
updatedAt: Schema.Attribute.DateTime;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
};
}
export interface ApiOrderOrder extends Struct.CollectionTypeSchema {
collectionName: 'orders';
info: {
@ -484,6 +465,7 @@ export interface ApiOrderOrder extends Struct.CollectionTypeSchema {
draftAndPublish: true;
};
attributes: {
block: Schema.Attribute.Relation<'manyToOne', 'api::block.block'>;
client: Schema.Attribute.Relation<'manyToOne', 'api::customer.customer'>;
createdAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
@ -518,7 +500,7 @@ export interface ApiServiceService extends Struct.CollectionTypeSchema {
collectionName: 'services';
info: {
description: '';
displayName: 'Service';
displayName: 'service';
pluralName: 'services';
singularName: 'service';
};
@ -526,11 +508,9 @@ export interface ApiServiceService extends Struct.CollectionTypeSchema {
draftAndPublish: true;
};
attributes: {
active: Schema.Attribute.Boolean & Schema.Attribute.DefaultTo<false>;
createdAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
description: Schema.Attribute.Text;
duration: Schema.Attribute.Time &
Schema.Attribute.Required &
Schema.Attribute.DefaultTo<'01:00:00.000'>;
@ -541,19 +521,8 @@ export interface ApiServiceService extends Struct.CollectionTypeSchema {
> &
Schema.Attribute.Private;
master: Schema.Attribute.Relation<'manyToOne', 'api::customer.customer'>;
name: Schema.Attribute.String &
Schema.Attribute.Required &
Schema.Attribute.SetMinMaxLength<{
maxLength: 100;
}>;
name: Schema.Attribute.String;
orders: Schema.Attribute.Relation<'manyToMany', 'api::order.order'>;
price: Schema.Attribute.Decimal &
Schema.Attribute.SetMinMax<
{
min: 1;
},
number
>;
publishedAt: Schema.Attribute.DateTime;
updatedAt: Schema.Attribute.DateTime;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
@ -561,6 +530,36 @@ export interface ApiServiceService extends Struct.CollectionTypeSchema {
};
}
export interface ApiSettingSetting extends Struct.CollectionTypeSchema {
collectionName: 'settings';
info: {
description: '';
displayName: 'Setting';
pluralName: 'settings';
singularName: 'setting';
};
options: {
draftAndPublish: true;
};
attributes: {
createdAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
locale: Schema.Attribute.String & Schema.Attribute.Private;
localizations: Schema.Attribute.Relation<
'oneToMany',
'api::setting.setting'
> &
Schema.Attribute.Private;
publishedAt: Schema.Attribute.DateTime;
recording_by_blocks: Schema.Attribute.Boolean &
Schema.Attribute.DefaultTo<false>;
updatedAt: Schema.Attribute.DateTime;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
};
}
export interface ApiSlotSlot extends Struct.CollectionTypeSchema {
collectionName: 'slots';
info: {
@ -591,212 +590,6 @@ export interface ApiSlotSlot extends Struct.CollectionTypeSchema {
};
}
export interface ApiSubscriptionHistorySubscriptionHistory
extends Struct.CollectionTypeSchema {
collectionName: 'subscription_histories';
info: {
displayName: 'SubscriptionHistory';
pluralName: 'subscription-histories';
singularName: 'subscription-history';
};
options: {
draftAndPublish: true;
};
attributes: {
amount: Schema.Attribute.Decimal & Schema.Attribute.Required;
createdAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
currency: Schema.Attribute.String & Schema.Attribute.DefaultTo<'RUB'>;
description: Schema.Attribute.Text;
locale: Schema.Attribute.String & Schema.Attribute.Private;
localizations: Schema.Attribute.Relation<
'oneToMany',
'api::subscription-history.subscription-history'
> &
Schema.Attribute.Private;
paymentId: Schema.Attribute.String;
period: Schema.Attribute.String &
Schema.Attribute.SetMinMaxLength<{
maxLength: 20;
}>;
publishedAt: Schema.Attribute.DateTime;
source: Schema.Attribute.Enumeration<
['payment', 'trial', 'reward', 'admin', 'renewal']
> &
Schema.Attribute.Required &
Schema.Attribute.DefaultTo<'payment'>;
state: Schema.Attribute.Enumeration<['success', 'failed', 'pending']> &
Schema.Attribute.Required;
subscription: Schema.Attribute.Relation<
'oneToOne',
'api::subscription.subscription'
>;
subscription_price: Schema.Attribute.Relation<
'oneToOne',
'api::subscription-price.subscription-price'
>;
updatedAt: Schema.Attribute.DateTime;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
};
}
export interface ApiSubscriptionPriceSubscriptionPrice
extends Struct.CollectionTypeSchema {
collectionName: 'subscription_prices';
info: {
displayName: 'SubscriptionPrice';
pluralName: 'subscription-prices';
singularName: 'subscription-price';
};
options: {
draftAndPublish: true;
};
attributes: {
active: Schema.Attribute.Boolean & Schema.Attribute.DefaultTo<true>;
amount: Schema.Attribute.Decimal & Schema.Attribute.Required;
createdAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
currency: Schema.Attribute.String & Schema.Attribute.DefaultTo<'RUB'>;
days: Schema.Attribute.Integer & Schema.Attribute.Required;
description: Schema.Attribute.Text;
locale: Schema.Attribute.String & Schema.Attribute.Private;
localizations: Schema.Attribute.Relation<
'oneToMany',
'api::subscription-price.subscription-price'
> &
Schema.Attribute.Private;
period: Schema.Attribute.Enumeration<
['trial', 'day', 'week', 'month', 'half_year', 'year']
> &
Schema.Attribute.Required;
publishedAt: Schema.Attribute.DateTime;
updatedAt: Schema.Attribute.DateTime;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
};
}
export interface ApiSubscriptionRewardSubscriptionReward
extends Struct.CollectionTypeSchema {
collectionName: 'subscription_rewards';
info: {
displayName: 'SubscriptionReward';
pluralName: 'subscription-rewards';
singularName: 'subscription-reward';
};
options: {
draftAndPublish: true;
};
attributes: {
activated: Schema.Attribute.Boolean & Schema.Attribute.DefaultTo<false>;
createdAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
days: Schema.Attribute.Integer & Schema.Attribute.Required;
description: Schema.Attribute.Text;
expiresAt: Schema.Attribute.DateTime & Schema.Attribute.Required;
invited: Schema.Attribute.Relation<'oneToOne', 'api::customer.customer'>;
locale: Schema.Attribute.String & Schema.Attribute.Private;
localizations: Schema.Attribute.Relation<
'oneToMany',
'api::subscription-reward.subscription-reward'
> &
Schema.Attribute.Private;
owner: Schema.Attribute.Relation<'manyToOne', 'api::customer.customer'>;
publishedAt: Schema.Attribute.DateTime;
subscription: Schema.Attribute.Relation<
'manyToOne',
'api::subscription.subscription'
>;
updatedAt: Schema.Attribute.DateTime;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
};
}
export interface ApiSubscriptionSettingSubscriptionSetting
extends Struct.SingleTypeSchema {
collectionName: 'subscription_settings';
info: {
displayName: 'SubscriptionSettings';
pluralName: 'subscription-settings';
singularName: 'subscription-setting';
};
options: {
draftAndPublish: true;
};
attributes: {
createdAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
locale: Schema.Attribute.String & Schema.Attribute.Private;
localizations: Schema.Attribute.Relation<
'oneToMany',
'api::subscription-setting.subscription-setting'
> &
Schema.Attribute.Private;
maxOrdersPerMonth: Schema.Attribute.Integer &
Schema.Attribute.Required &
Schema.Attribute.DefaultTo<20>;
proEnabled: Schema.Attribute.Boolean & Schema.Attribute.DefaultTo<false>;
publishedAt: Schema.Attribute.DateTime;
referralRewardDays: Schema.Attribute.Integer &
Schema.Attribute.Required &
Schema.Attribute.DefaultTo<1>;
updatedAt: Schema.Attribute.DateTime;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
};
}
export interface ApiSubscriptionSubscription
extends Struct.CollectionTypeSchema {
collectionName: 'subscriptions';
info: {
displayName: 'Subscription';
pluralName: 'subscriptions';
singularName: 'subscription';
};
options: {
draftAndPublish: true;
};
attributes: {
active: Schema.Attribute.Boolean &
Schema.Attribute.Required &
Schema.Attribute.DefaultTo<false>;
createdAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
customer: Schema.Attribute.Relation<'manyToOne', 'api::customer.customer'>;
expiresAt: Schema.Attribute.DateTime & Schema.Attribute.Required;
locale: Schema.Attribute.String & Schema.Attribute.Private;
localizations: Schema.Attribute.Relation<
'oneToMany',
'api::subscription.subscription'
> &
Schema.Attribute.Private;
nextSubscription: Schema.Attribute.Relation<
'oneToOne',
'api::subscription.subscription'
>;
publishedAt: Schema.Attribute.DateTime;
subscription_history: Schema.Attribute.Relation<
'oneToOne',
'api::subscription-history.subscription-history'
>;
subscription_rewards: Schema.Attribute.Relation<
'oneToMany',
'api::subscription-reward.subscription-reward'
>;
updatedAt: Schema.Attribute.DateTime;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
};
}
export interface PluginContentReleasesRelease
extends Struct.CollectionTypeSchema {
collectionName: 'strapi_releases';
@ -1306,16 +1099,12 @@ declare module '@strapi/strapi' {
'admin::transfer-token': AdminTransferToken;
'admin::transfer-token-permission': AdminTransferTokenPermission;
'admin::user': AdminUser;
'api::block.block': ApiBlockBlock;
'api::customer.customer': ApiCustomerCustomer;
'api::customer.customer-setting': ApiCustomerCustomerSetting;
'api::order.order': ApiOrderOrder;
'api::service.service': ApiServiceService;
'api::setting.setting': ApiSettingSetting;
'api::slot.slot': ApiSlotSlot;
'api::subscription-history.subscription-history': ApiSubscriptionHistorySubscriptionHistory;
'api::subscription-price.subscription-price': ApiSubscriptionPriceSubscriptionPrice;
'api::subscription-reward.subscription-reward': ApiSubscriptionRewardSubscriptionReward;
'api::subscription-setting.subscription-setting': ApiSubscriptionSettingSubscriptionSetting;
'api::subscription.subscription': ApiSubscriptionSubscription;
'plugin::content-releases.release': PluginContentReleasesRelease;
'plugin::content-releases.release-action': PluginContentReleasesReleaseAction;
'plugin::i18n.locale': PluginI18NLocale;