Compare commits

...

18 Commits

Author SHA1 Message Date
vchikalkin
6b788a9024 add docker-compose.dev.yml 2025-10-11 14:16:05 +03:00
vchikalkin
0e58a8cef7 subscription-price: add period 'day' 2025-10-09 16:19:03 +03:00
vchikalkin
a63eff9f46 customer: add field surname 2025-10-07 11:42:27 +03:00
Vlad Chikalkin
e0a9f55eeb
Feature/pro subscription (#8)
* feat: add subscription relation to customer schema and new subscription-related interfaces

- Introduced a one-to-one relation for 'subscription' in the customer schema to link customers with their subscriptions.
- Added new TypeScript interfaces for subscription-related entities, including Subscription, SubscriptionHistory, SubscriptionPrice, and SubscriptionSetting, ensuring type safety and consistency across the application.

* refactor: remove unused fields from subscription schema and TypeScript definitions

- Deleted the 'referralCode', 'subscription', 'referralCount', and 'referredBy' fields from the subscription schema to streamline the data model.
- Updated TypeScript definitions to reflect these changes, ensuring type safety and consistency across the application.

* feat: add 'days' field to subscription price schema and update TypeScript definitions

- Introduced a new 'days' integer field in the subscription price schema, marked as required to enhance subscription duration tracking.
- Removed unused 'description', 'trialPeriodDays', and 'trialEnabled' fields from the subscription setting schema to streamline the data model.
- Updated TypeScript definitions to reflect these changes, ensuring type safety and consistency across the application.

* refactor: rename relation fields in customer schema and update TypeScript definitions

- Changed 'clients' to 'invited' and 'masters' to 'invitedBy' in the customer schema to better reflect their purpose in the data model.
- Updated TypeScript definitions to include the new relation fields, ensuring type safety and consistency across the application.

* feat: add subscription_rewards relation to customer and subscription history schemas, and update TypeScript definitions

- Introduced a new 'subscription_rewards' relation in both the customer and subscription history schemas to link them with subscription rewards.
- Updated the 'expiresAt' field in the subscription schema to be required, ensuring better data integrity.
- Adjusted the default value for 'maxOrdersPerMonth' in the subscription setting schema from 30 to 20, and removed unused fields to streamline the model.
- Updated TypeScript definitions to reflect these changes, ensuring type safety and consistency across the application.

* SubscriptionPrice: rename price -> amount

* refactor: remove block-related schemas, controllers, routes, and services

- Deleted the block schema, controller, routes, and services to streamline the API structure.
- Updated customer and order schemas to remove references to the block relation, ensuring data integrity and clarity.
- Adjusted TypeScript definitions to reflect the removal of block-related entities, maintaining type safety across the application.

* SubscriptionHistory: change type to string

* add new entity customer_setting

* subscription settings: add field proEnabled
2025-09-17 14:46:39 +03:00
Vlad Chikalkin
ccd0947334
feat: add bannedUntil field to customer schema (#7)
- Introduced a new 'bannedUntil' datetime field in the customer schema to track when a customer is banned until.
- Updated TypeScript definitions to include the new 'bannedUntil' attribute, ensuring type safety and consistency across the application.
2025-08-26 13:18:12 +03:00
vchikalkin
37c28353a9 feat: enhance Telegram notification to include total service cost
- Updated the Telegram notification message to display the total cost of services associated with an order, improving transparency for clients and masters.
- Added logic to calculate the total price based on individual service prices and formatted the price for better readability.
2025-08-20 17:59:36 +03:00
vchikalkin
01fbd1c696 feat: add price and description fields to service schema
- Introduced new fields 'price' and 'description' in the service schema to enhance service details.
- Updated TypeScript definitions to include these new attributes, ensuring type safety and consistency across the application.
2025-08-20 17:23:33 +03:00
vchikalkin
8a81a1e251 feat: update Telegram notification to list all services for orders
- Modified the notification message format to include a list of all services associated with an order instead of just the first service. This change enhances clarity for both clients and masters by providing comprehensive service details in notifications.
2025-08-19 19:27:54 +03:00
vchikalkin
3fb75f5765 .gitignore: add *.cmd 2025-08-19 19:22:10 +03:00
vchikalkin
d9ac6858e7 chore: update Docker image configuration and enhance CI/CD workflow
- Changed the Docker image reference in docker-compose.yml to use dynamic environment variables for better flexibility.
- Updated the GitHub Actions workflow to set the image tag based on the commit SHA, ensuring unique tagging for each build.
- Added environment variable definitions for Docker Hub credentials and image tag in the deployment process.
2025-08-15 21:17:31 +03:00
Vlad Chikalkin
9f1b13192f
Issues/76 (#6)
* refactor: consolidate order lifecycle validation logic and remove slot lifecycle

- Streamlined the order lifecycle by consolidating validation logic and removing redundant error handling.
- Deleted the slot lifecycle file to simplify the codebase, as its functionality is no longer needed.
- This change enhances maintainability and reduces complexity in the order management system.

* feat: enhance Telegram notification heading for order updates

- Updated the heading emoji logic to display a checkered flag for completed orders and a pencil for updates, improving clarity in notifications.
- Added specific handling for completed records in the notification heading to differentiate between updated and completed statuses.
2025-08-11 16:25:56 +03:00
vchikalkin
b0aa644435 feat: add update check to order and slot lifecycles
- Implemented a check in both order and slot lifecycles to skip validation if the record is being updated, identified by the presence of the 'publishedAt' field. This change prevents unnecessary validation for existing records.
2025-08-03 13:56:43 +03:00
vchikalkin
63a68c6089 refactor: comment out beforeDelete lifecycle method in slot API
- Temporarily disabled the beforeDelete method to prevent slot deletion checks related to existing orders. This change is intended for further review and potential reimplementation.
2025-08-02 19:26:38 +03:00
vchikalkin
7a89cfa7ce fix: update slot start time validation to check against the current day
- Modified the validation logic to ensure that the slot start time is not before the current day, preventing past slots from being created.
2025-08-02 18:37:42 +03:00
vchikalkin
cfa5a02ca9 feat: add validation to prevent order creation in the past
- Introduced a new error message for attempts to create orders with a start time in the past.
- Implemented a check in the order lifecycle to validate that the order start time is not before the current time.
2025-08-02 11:19:21 +03:00
vchikalkin
50f23f67b3 feat: update customer and service schemas to enforce maxLength and required attributes
- Added maxLength constraints to 'name' and 'phone' attributes in customer and service schemas.
- Marked 'name' as required in both schemas.
- Updated TypeScript definitions to reflect these changes.
2025-08-02 11:15:21 +03:00
vchikalkin
3833a1a23d feat: enhance extractId function to support 'connect' input for ID retrieval 2025-08-02 11:04:53 +03:00
Vlad Chikalkin
d592c9cf31
feat: add 'active' boolean attribute to content type schema and TypeScript definitions (#5) 2025-08-01 19:53:47 +03:00
40 changed files with 784 additions and 580 deletions

View File

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

4
.gitignore vendored
View File

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

28
docker-compose.dev.yml Normal file
View File

@ -0,0 +1,28 @@
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: services:
strapi: strapi:
image: vchikalkin/zapishis-strapi:latest image: ${DOCKERHUB_USERNAME}/zapishis-strapi:${STRAPI_IMAGE_TAG}
ports: ports:
- "127.0.0.1:1337:1337" - "127.0.0.1:1337:1337"
environment: environment:

View File

@ -1,60 +0,0 @@
{
"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

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

View File

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

View File

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

View File

@ -0,0 +1,24 @@
{
"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,47 +14,48 @@
"attributes": { "attributes": {
"name": { "name": {
"type": "string", "type": "string",
"maxLength": 100,
"required": true "required": true
}, },
"surname": {
"type": "string",
"maxLength": 100,
"required": false
},
"telegramId": { "telegramId": {
"type": "biginteger", "type": "biginteger",
"unique": true "unique": true
}, },
"phone": { "phone": {
"type": "string", "type": "string",
"unique": true, "maxLength": 20,
"required": true "required": true,
"unique": true
}, },
"role": { "role": {
"type": "enumeration", "type": "enumeration",
"required": true,
"enum": [ "enum": [
"client", "client",
"master" "master"
], ]
"required": true
}, },
"active": { "active": {
"type": "boolean", "type": "boolean",
"default": false, "required": false,
"required": false "default": false
}, },
"clients": { "invited": {
"type": "relation", "type": "relation",
"relation": "manyToMany", "relation": "manyToMany",
"target": "api::customer.customer", "target": "api::customer.customer",
"inversedBy": "masters" "inversedBy": "invitedBy"
}, },
"masters": { "invitedBy": {
"type": "relation", "type": "relation",
"relation": "manyToMany", "relation": "manyToMany",
"target": "api::customer.customer", "target": "api::customer.customer",
"mappedBy": "clients" "mappedBy": "invited"
},
"blocks": {
"type": "relation",
"relation": "oneToMany",
"target": "api::block.block",
"mappedBy": "client"
}, },
"slots": { "slots": {
"type": "relation", "type": "relation",
@ -76,6 +77,27 @@
"relation": "oneToMany", "relation": "oneToMany",
"target": "api::service.service", "target": "api::service.service",
"mappedBy": "master" "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,23 +1,6 @@
import { bot, extractId, dayjs } from '../../../../utils'; import { bot, extractId, dayjs } from '../../../../utils';
import { DEFAULT_TZ } from '../../../../constants'; 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> = { const STATE_MAP: Record<State, string> = {
approved: 'Подтверждено', approved: 'Подтверждено',
cancelled: 'Отменено', cancelled: 'Отменено',
@ -86,13 +69,20 @@ async function sendTelegramNotification(orderEntity: Order, isUpdate = false) {
const emojiForState = EMOJI_MAP[state] || ''; const emojiForState = EMOJI_MAP[state] || '';
const stateLabel = STATE_MAP[state] || state; const stateLabel = STATE_MAP[state] || state;
// Эмодзи в заголовке: карандаш при обновлении, иначе эмодзи статуса // Эмодзи в заголовке: карандаш при обновлении, флаг для завершенных, иначе эмодзи статуса
const headingEmoji = isUpdate ? '✏️' : emojiForState; const headingEmoji = isUpdate
? (state === 'completed' ? '🏁' : '✏️')
: emojiForState;
let heading = ''; let heading = '';
if (isUpdate) { if (isUpdate) {
heading = `${headingEmoji} <b>Запись изменена</b>`; // Специальная обработка для завершенных записей
if (state === 'completed') {
heading = `${headingEmoji} <b>Запись завершена</b>`;
} else {
heading = `${headingEmoji} <b>Запись изменена</b>`;
}
} else { } else {
const isApproved = state === 'approved'; const isApproved = state === 'approved';
const creationText = isApproved const creationText = isApproved
@ -116,20 +106,37 @@ async function sendTelegramNotification(orderEntity: Order, isUpdate = false) {
const clientName = order.client?.name || '-'; const clientName = order.client?.name || '-';
const masterName = slot.master?.name || '-'; const masterName = slot.master?.name || '-';
const serviceName = order.services?.[0]?.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 messageForMaster = `${heading} const messageForMaster = `${heading}
<b>Дата:</b> ${date} <b>Дата:</b> ${date}
<b>Время:</b> ${timeStartString} - ${timeEndString} <b>Время:</b> ${timeStartString} - ${timeEndString}
<b>Клиент:</b> ${clientName} <b>Клиент:</b> ${clientName}
<b>Услуга:</b> ${serviceName} <b>Услуги:</b> ${servicesList}
<b>Стоимость:</b> ${priceText}
<b>Статус:</b> ${emojiForState} ${stateLabel}`; <b>Статус:</b> ${emojiForState} ${stateLabel}`;
const messageForClient = `${heading} const messageForClient = `${heading}
<b>Дата:</b> ${date} <b>Дата:</b> ${date}
<b>Время:</b> ${timeStartString} - ${timeEndString} <b>Время:</b> ${timeStartString} - ${timeEndString}
<b>Мастер:</b> ${masterName} <b>Мастер:</b> ${masterName}
<b>Услуга:</b> ${serviceName} <b>Услуги:</b> ${servicesList}
<b>Стоимость:</b> ${priceText}
<b>Статус:</b> ${emojiForState} ${stateLabel}`; <b>Статус:</b> ${emojiForState} ${stateLabel}`;
if (masterTelegramId) { if (masterTelegramId) {
@ -169,189 +176,4 @@ export default {
await sendTelegramNotification(updatedEntity, true); 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,12 +30,6 @@
"target": "api::customer.customer", "target": "api::customer.customer",
"inversedBy": "orders" "inversedBy": "orders"
}, },
"block": {
"type": "relation",
"relation": "manyToOne",
"target": "api::block.block",
"inversedBy": "orders"
},
"order_number": { "order_number": {
"type": "integer" "type": "integer"
}, },

View File

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

View File

@ -1,20 +0,0 @@
{
"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

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

View File

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

View File

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

View File

@ -1,144 +0,0 @@
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

@ -0,0 +1,67 @@
{
"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

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

View File

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

View File

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

View File

@ -0,0 +1,48 @@
{
"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

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

View File

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

View File

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

View File

@ -0,0 +1,47 @@
{
"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

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

View File

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

View File

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

View File

@ -0,0 +1,29 @@
{
"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

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

View File

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

View File

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

View File

@ -0,0 +1,47 @@
{
"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

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

View File

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

View File

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

View File

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

View File

@ -373,43 +373,6 @@ 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 { export interface ApiCustomerCustomer extends Struct.CollectionTypeSchema {
collectionName: 'customers'; collectionName: 'customers';
info: { info: {
@ -423,29 +386,55 @@ export interface ApiCustomerCustomer extends Struct.CollectionTypeSchema {
}; };
attributes: { attributes: {
active: Schema.Attribute.Boolean & Schema.Attribute.DefaultTo<false>; active: Schema.Attribute.Boolean & Schema.Attribute.DefaultTo<false>;
blocks: Schema.Attribute.Relation<'oneToMany', 'api::block.block'>; bannedUntil: Schema.Attribute.DateTime;
clients: Schema.Attribute.Relation<'manyToMany', 'api::customer.customer'>;
createdAt: Schema.Attribute.DateTime; createdAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> & createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private; 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; locale: Schema.Attribute.String & Schema.Attribute.Private;
localizations: Schema.Attribute.Relation< localizations: Schema.Attribute.Relation<
'oneToMany', 'oneToMany',
'api::customer.customer' 'api::customer.customer'
> & > &
Schema.Attribute.Private; Schema.Attribute.Private;
masters: Schema.Attribute.Relation<'manyToMany', 'api::customer.customer'>; name: Schema.Attribute.String &
name: Schema.Attribute.String & Schema.Attribute.Required; Schema.Attribute.Required &
Schema.Attribute.SetMinMaxLength<{
maxLength: 100;
}>;
orders: Schema.Attribute.Relation<'oneToMany', 'api::order.order'>; orders: Schema.Attribute.Relation<'oneToMany', 'api::order.order'>;
phone: Schema.Attribute.String & phone: Schema.Attribute.String &
Schema.Attribute.Required & Schema.Attribute.Required &
Schema.Attribute.Unique; Schema.Attribute.Unique &
Schema.Attribute.SetMinMaxLength<{
maxLength: 20;
}>;
photoUrl: Schema.Attribute.String; photoUrl: Schema.Attribute.String;
publishedAt: Schema.Attribute.DateTime; publishedAt: Schema.Attribute.DateTime;
role: Schema.Attribute.Enumeration<['client', 'master']> & role: Schema.Attribute.Enumeration<['client', 'master']> &
Schema.Attribute.Required; Schema.Attribute.Required;
services: Schema.Attribute.Relation<'oneToMany', 'api::service.service'>; services: Schema.Attribute.Relation<'oneToMany', 'api::service.service'>;
slots: Schema.Attribute.Relation<'oneToMany', 'api::slot.slot'>; 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; telegramId: Schema.Attribute.BigInteger & Schema.Attribute.Unique;
updatedAt: Schema.Attribute.DateTime; updatedAt: Schema.Attribute.DateTime;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> & updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
@ -453,6 +442,36 @@ 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 { export interface ApiOrderOrder extends Struct.CollectionTypeSchema {
collectionName: 'orders'; collectionName: 'orders';
info: { info: {
@ -465,7 +484,6 @@ export interface ApiOrderOrder extends Struct.CollectionTypeSchema {
draftAndPublish: true; draftAndPublish: true;
}; };
attributes: { attributes: {
block: Schema.Attribute.Relation<'manyToOne', 'api::block.block'>;
client: Schema.Attribute.Relation<'manyToOne', 'api::customer.customer'>; client: Schema.Attribute.Relation<'manyToOne', 'api::customer.customer'>;
createdAt: Schema.Attribute.DateTime; createdAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> & createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
@ -500,7 +518,7 @@ export interface ApiServiceService extends Struct.CollectionTypeSchema {
collectionName: 'services'; collectionName: 'services';
info: { info: {
description: ''; description: '';
displayName: 'service'; displayName: 'Service';
pluralName: 'services'; pluralName: 'services';
singularName: 'service'; singularName: 'service';
}; };
@ -508,9 +526,11 @@ export interface ApiServiceService extends Struct.CollectionTypeSchema {
draftAndPublish: true; draftAndPublish: true;
}; };
attributes: { attributes: {
active: Schema.Attribute.Boolean & Schema.Attribute.DefaultTo<false>;
createdAt: Schema.Attribute.DateTime; createdAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> & createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private; Schema.Attribute.Private;
description: Schema.Attribute.Text;
duration: Schema.Attribute.Time & duration: Schema.Attribute.Time &
Schema.Attribute.Required & Schema.Attribute.Required &
Schema.Attribute.DefaultTo<'01:00:00.000'>; Schema.Attribute.DefaultTo<'01:00:00.000'>;
@ -521,8 +541,19 @@ export interface ApiServiceService extends Struct.CollectionTypeSchema {
> & > &
Schema.Attribute.Private; Schema.Attribute.Private;
master: Schema.Attribute.Relation<'manyToOne', 'api::customer.customer'>; master: Schema.Attribute.Relation<'manyToOne', 'api::customer.customer'>;
name: Schema.Attribute.String; name: Schema.Attribute.String &
Schema.Attribute.Required &
Schema.Attribute.SetMinMaxLength<{
maxLength: 100;
}>;
orders: Schema.Attribute.Relation<'manyToMany', 'api::order.order'>; orders: Schema.Attribute.Relation<'manyToMany', 'api::order.order'>;
price: Schema.Attribute.Decimal &
Schema.Attribute.SetMinMax<
{
min: 1;
},
number
>;
publishedAt: Schema.Attribute.DateTime; publishedAt: Schema.Attribute.DateTime;
updatedAt: Schema.Attribute.DateTime; updatedAt: Schema.Attribute.DateTime;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> & updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
@ -530,36 +561,6 @@ 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 { export interface ApiSlotSlot extends Struct.CollectionTypeSchema {
collectionName: 'slots'; collectionName: 'slots';
info: { info: {
@ -590,6 +591,212 @@ 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 export interface PluginContentReleasesRelease
extends Struct.CollectionTypeSchema { extends Struct.CollectionTypeSchema {
collectionName: 'strapi_releases'; collectionName: 'strapi_releases';
@ -1099,12 +1306,16 @@ declare module '@strapi/strapi' {
'admin::transfer-token': AdminTransferToken; 'admin::transfer-token': AdminTransferToken;
'admin::transfer-token-permission': AdminTransferTokenPermission; 'admin::transfer-token-permission': AdminTransferTokenPermission;
'admin::user': AdminUser; 'admin::user': AdminUser;
'api::block.block': ApiBlockBlock;
'api::customer.customer': ApiCustomerCustomer; 'api::customer.customer': ApiCustomerCustomer;
'api::customer.customer-setting': ApiCustomerCustomerSetting;
'api::order.order': ApiOrderOrder; 'api::order.order': ApiOrderOrder;
'api::service.service': ApiServiceService; 'api::service.service': ApiServiceService;
'api::setting.setting': ApiSettingSetting;
'api::slot.slot': ApiSlotSlot; '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': PluginContentReleasesRelease;
'plugin::content-releases.release-action': PluginContentReleasesReleaseAction; 'plugin::content-releases.release-action': PluginContentReleasesReleaseAction;
'plugin::i18n.locale': PluginI18NLocale; 'plugin::i18n.locale': PluginI18NLocale;