Compare commits

..

8 Commits

47 changed files with 573 additions and 1017 deletions

View File

@ -9,25 +9,19 @@ 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:${{ steps.vars.outputs.tag }} . run: docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/zapishis-strapi:latest .
- name: Push image to Docker Hub - 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: deploy:
name: Deploy to VPS name: Deploy to VPS
@ -63,9 +57,6 @@ jobs:
echo "DATABASE_USERNAME=${{ secrets.DATABASE_USERNAME }}" >> .env echo "DATABASE_USERNAME=${{ secrets.DATABASE_USERNAME }}" >> .env
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 "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,6 +128,4 @@ exports
dist dist
build build
.strapi-updater.json .strapi-updater.json
.strapi-cloud.json .strapi-cloud.json
*.cmd

View File

@ -1,7 +1,6 @@
export default ({ env }) => ({ export default ({ env }) => ({
host: env('HOST', '0.0.0.0'), host: env('HOST', '0.0.0.0'),
port: env.int('PORT', 1337), port: env.int('PORT', 1337),
botToken: env('BOT_TOKEN', undefined),
app: { app: {
keys: env.array('APP_KEYS'), 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: services:
strapi: strapi:
image: ${DOCKERHUB_USERNAME}/zapishis-strapi:${STRAPI_IMAGE_TAG} image: vchikalkin/zapishis-strapi:latest
ports: ports:
- "127.0.0.1:1337:1337" - "127.0.0.1:1337:1337"
environment: environment:
@ -15,7 +15,6 @@ services:
DATABASE_USERNAME: ${DATABASE_USERNAME} DATABASE_USERNAME: ${DATABASE_USERNAME}
DATABASE_PASSWORD: ${DATABASE_PASSWORD} DATABASE_PASSWORD: ${DATABASE_PASSWORD}
DATABASE_SSL: "false" DATABASE_SSL: "false"
BOT_TOKEN: ${BOT_TOKEN}
networks: networks:
- app - app

View File

@ -20,8 +20,7 @@
"react": "^18.0.0", "react": "^18.0.0",
"react-dom": "^18.0.0", "react-dom": "^18.0.0",
"react-router-dom": "^6.0.0", "react-router-dom": "^6.0.0",
"styled-components": "^6.0.0", "styled-components": "^6.0.0"
"telegraf": "^4.16.3"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "^20",

84
pnpm-lock.yaml generated
View File

@ -38,9 +38,6 @@ importers:
styled-components: styled-components:
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
telegraf:
specifier: ^4.16.3
version: 4.16.3
devDependencies: devDependencies:
'@types/node': '@types/node':
specifier: ^20 specifier: ^20
@ -1903,9 +1900,6 @@ packages:
resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==}
engines: {node: '>=10'} engines: {node: '>=10'}
'@telegraf/types@7.1.0':
resolution: {integrity: sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw==}
'@testing-library/dom@10.1.0': '@testing-library/dom@10.1.0':
resolution: {integrity: sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA==} resolution: {integrity: sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -2191,10 +2185,6 @@ packages:
'@xtuc/long@4.2.2': '@xtuc/long@4.2.2':
resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==}
abort-controller@3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
engines: {node: '>=6.5'}
accepts@1.3.8: accepts@1.3.8:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@ -2432,18 +2422,9 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true hasBin: true
buffer-alloc-unsafe@1.1.0:
resolution: {integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==}
buffer-alloc@1.2.0:
resolution: {integrity: sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==}
buffer-equal-constant-time@1.0.1: buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
buffer-fill@1.0.0:
resolution: {integrity: sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==}
buffer-from@1.1.2: buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
@ -3184,10 +3165,6 @@ packages:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
event-target-shim@5.0.1:
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
engines: {node: '>=6'}
events@3.3.0: events@3.3.0:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'} engines: {node: '>=0.8.x'}
@ -4438,10 +4415,6 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
mrmime@2.0.0: mrmime@2.0.0:
resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -4687,10 +4660,6 @@ packages:
resolution: {integrity: sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg==} resolution: {integrity: sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg==}
engines: {node: '>=12'} engines: {node: '>=12'}
p-timeout@4.1.0:
resolution: {integrity: sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw==}
engines: {node: '>=10'}
p-try@2.2.0: p-try@2.2.0:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -5376,9 +5345,6 @@ packages:
safe-buffer@5.2.1: safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
safe-compare@1.1.4:
resolution: {integrity: sha512-b9wZ986HHCo/HbKrRpBJb2kqXMK9CEWIE1egeEvZsYn69ay3kdfl9nG3RyOcR+jInTDf7a86WQ1d4VJX7goSSQ==}
safe-stable-stringify@2.5.0: safe-stable-stringify@2.5.0:
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -5386,10 +5352,6 @@ packages:
safer-buffer@2.1.2: safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
sandwich-stream@2.0.2:
resolution: {integrity: sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ==}
engines: {node: '>= 0.10'}
sanitize-html@2.13.0: sanitize-html@2.13.0:
resolution: {integrity: sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==} resolution: {integrity: sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==}
@ -5728,11 +5690,6 @@ packages:
resolution: {integrity: sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==} resolution: {integrity: sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==}
engines: {node: '>=8.0.0'} engines: {node: '>=8.0.0'}
telegraf@4.16.3:
resolution: {integrity: sha512-yjEu2NwkHlXu0OARWoNhJlIjX09dRktiMQFsM678BAH/PEPVwctzL67+tvXqLCRQQvm3SDtki2saGO9hLlz68w==}
engines: {node: ^12.20.0 || >=14.13.1}
hasBin: true
terser-webpack-plugin@5.3.10: terser-webpack-plugin@5.3.10:
resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==}
engines: {node: '>= 10.13.0'} engines: {node: '>= 10.13.0'}
@ -8866,8 +8823,6 @@ snapshots:
dependencies: dependencies:
defer-to-connect: 2.0.1 defer-to-connect: 2.0.1
'@telegraf/types@7.1.0': {}
'@testing-library/dom@10.1.0': '@testing-library/dom@10.1.0':
dependencies: dependencies:
'@babel/code-frame': 7.26.2 '@babel/code-frame': 7.26.2
@ -9246,10 +9201,6 @@ snapshots:
'@xtuc/long@4.2.2': {} '@xtuc/long@4.2.2': {}
abort-controller@3.0.0:
dependencies:
event-target-shim: 5.0.1
accepts@1.3.8: accepts@1.3.8:
dependencies: dependencies:
mime-types: 2.1.35 mime-types: 2.1.35
@ -9499,17 +9450,8 @@ snapshots:
node-releases: 2.0.18 node-releases: 2.0.18
update-browserslist-db: 1.1.1(browserslist@4.24.2) update-browserslist-db: 1.1.1(browserslist@4.24.2)
buffer-alloc-unsafe@1.1.0: {}
buffer-alloc@1.2.0:
dependencies:
buffer-alloc-unsafe: 1.1.0
buffer-fill: 1.0.0
buffer-equal-constant-time@1.0.1: {} buffer-equal-constant-time@1.0.1: {}
buffer-fill@1.0.0: {}
buffer-from@1.1.2: {} buffer-from@1.1.2: {}
buffer-writer@2.0.0: {} buffer-writer@2.0.0: {}
@ -10300,8 +10242,6 @@ snapshots:
etag@1.8.1: {} etag@1.8.1: {}
event-target-shim@5.0.1: {}
events@3.3.0: {} events@3.3.0: {}
eventsource@2.0.2: {} eventsource@2.0.2: {}
@ -11663,8 +11603,6 @@ snapshots:
mkdirp@3.0.1: {} mkdirp@3.0.1: {}
mri@1.2.0: {}
mrmime@2.0.0: {} mrmime@2.0.0: {}
ms@2.0.0: {} ms@2.0.0: {}
@ -11941,8 +11879,6 @@ snapshots:
dependencies: dependencies:
aggregate-error: 4.0.1 aggregate-error: 4.0.1
p-timeout@4.1.0: {}
p-try@2.2.0: {} p-try@2.2.0: {}
package-json-from-dist@1.0.1: {} package-json-from-dist@1.0.1: {}
@ -12676,16 +12612,10 @@ snapshots:
safe-buffer@5.2.1: {} safe-buffer@5.2.1: {}
safe-compare@1.1.4:
dependencies:
buffer-alloc: 1.2.0
safe-stable-stringify@2.5.0: {} safe-stable-stringify@2.5.0: {}
safer-buffer@2.1.2: {} safer-buffer@2.1.2: {}
sandwich-stream@2.0.2: {}
sanitize-html@2.13.0: sanitize-html@2.13.0:
dependencies: dependencies:
deepmerge: 4.3.1 deepmerge: 4.3.1
@ -13077,20 +13007,6 @@ snapshots:
tarn@3.0.2: {} tarn@3.0.2: {}
telegraf@4.16.3:
dependencies:
'@telegraf/types': 7.1.0
abort-controller: 3.0.0
debug: 4.3.7(supports-color@5.5.0)
mri: 1.2.0
node-fetch: 2.7.0
p-timeout: 4.1.0
safe-compare: 1.1.4
sandwich-stream: 2.0.2
transitivePeerDependencies:
- encoding
- supports-color
terser-webpack-plugin@5.3.10(esbuild@0.25.5)(webpack@5.96.1(esbuild@0.25.5)): terser-webpack-plugin@5.3.10(esbuild@0.25.5)(webpack@5.96.1(esbuild@0.25.5)):
dependencies: dependencies:
'@jridgewell/trace-mapping': 0.3.25 '@jridgewell/trace-mapping': 0.3.25

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": { "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",
"maxLength": 20, "unique": true,
"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",
"required": false, "default": false,
"default": false "required": false
}, },
"invited": { "clients": {
"type": "relation", "type": "relation",
"relation": "manyToMany", "relation": "manyToMany",
"target": "api::customer.customer", "target": "api::customer.customer",
"inversedBy": "invitedBy" "inversedBy": "masters"
}, },
"invitedBy": { "masters": {
"type": "relation", "type": "relation",
"relation": "manyToMany", "relation": "manyToMany",
"target": "api::customer.customer", "target": "api::customer.customer",
"mappedBy": "invited" "mappedBy": "clients"
},
"blocks": {
"type": "relation",
"relation": "oneToMany",
"target": "api::block.block",
"mappedBy": "client"
}, },
"slots": { "slots": {
"type": "relation", "type": "relation",
@ -77,27 +76,6 @@
"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,179 +1,212 @@
import { bot, extractId, dayjs } from '../../../../utils'; import { extractId } from '../../../../utils';
import { DEFAULT_TZ } from '../../../../constants';
const STATE_MAP: Record<State, string> = { const ERR_MISSING_TIME = 'Не указано время';
approved: 'Подтверждено', const ERR_INVALID_TIME = 'Некорректное время';
cancelled: 'Отменено', const ERR_OVERLAPPING_TIME = 'Время пересекается с другими заказами';
cancelling: 'Отменяется', const ERR_INACTIVE_CLIENT = 'Клиент не активен';
completed: 'Завершено', const ERR_INACTIVE_MASTER = 'Мастер не активен';
created: 'Создано', const ERR_SLOT_CLOSED = 'Слот закрыт';
scheduled: 'Запланировано', 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 EMOJI_MAP: Record<State, string> = { const dayjs = require('dayjs');
approved: '✅', const utc = require('dayjs/plugin/utc');
cancelled: '❌', const timezone = require('dayjs/plugin/timezone');
cancelling: '🟠',
completed: '🏁',
created: '📝',
scheduled: '📅',
};
type State = if (!dayjs.prototype.tz) {
| 'approved' dayjs.extend(utc);
| 'cancelled' dayjs.extend(timezone);
| 'cancelling' }
| 'completed'
| 'created'
| 'scheduled';
type Order = { export default {
id: number; async beforeCreate(event) {
documentId: string; const { data } = event.params;
state: State; const { datetime_start, datetime_end, client, services } = data;
order_number: number; const clientId = extractId(client);
createdAt: string; const slotId = extractId(data.slot);
updatedAt: string;
publishedAt: any;
locale: any;
datetime_start: string;
datetime_end: string;
};
async function sendTelegramNotification(orderEntity: Order, isUpdate = false) { // Проверка наличия обязательных полей
try { if (!slotId) throw new Error(ERR_MISSING_SLOT);
const order = await strapi.db.query('api::order.order').findOne({ if (!clientId) throw new Error(ERR_MISSING_CLIENT);
where: { id: orderEntity.id }, if (!extractId(services)) throw new Error(ERR_MISSING_SERVICE);
populate: ['client', 'slot', 'services'],
});
if (!order) throw new Error('Order not found'); // Проверка корректности времени заказа.
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 slotId = extractId(order.slot); // Получаем слот
const slot = await strapi.db.query('api::slot.slot').findOne({ const slot = await strapi.db.query('api::slot.slot').findOne({
where: { id: slotId }, where: { id: slotId },
populate: ['master'], populate: ['master'],
}); });
if (!slot) throw new Error(ERR_MISSING_SLOT);
if (!slot) throw new Error('Slot not found'); // Проверка, что заказ укладывается в рамки слота
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);
}
const clientTelegramId = order.client?.telegramId; // 1. Слот не должен быть закрыт
const masterTelegramId = slot.master?.telegramId; 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('Europe/Moscow');
const orderStart = dayjs(existingOrder.datetime_start).tz('Europe/Moscow');
if (state === 'completed' && now.isBefore(orderStart, 'minute')) {
throw new Error(ERR_CANNOT_COMPLETE_BEFORE_START);
}
if (!masterTelegramId) {
console.warn('Master telegram ID not found');
return; return;
} }
const state = orderEntity.state; if (
const emojiForState = EMOJI_MAP[state] || ''; existingOrder.datetime_start === datetime_start &&
const stateLabel = STATE_MAP[state] || state; existingOrder.datetime_end === datetime_end
) {
// Эмодзи в заголовке: карандаш при обновлении, флаг для завершенных, иначе эмодзи статуса return;
const headingEmoji = isUpdate
? (state === 'completed' ? '🏁' : '✏️')
: emojiForState;
let heading = '';
if (isUpdate) {
// Специальная обработка для завершенных записей
if (state === 'completed') {
heading = `${headingEmoji} <b>Запись завершена</b>`;
} else {
heading = `${headingEmoji} <b>Запись изменена</b>`;
}
} else {
const isApproved = state === 'approved';
const creationText = isApproved
? `Запись создана и подтверждена!`
: `Запись создана!`;
heading = `${headingEmoji} <b>${creationText}</b>`;
} }
const date = dayjs if (!datetime_start || !datetime_end) {
.utc(orderEntity.datetime_start) throw new Error(ERR_INVALID_TIME);
.tz(DEFAULT_TZ)
.format('D MMMM YYYY');
const timeStartString = dayjs
.utc(orderEntity.datetime_start)
.tz(DEFAULT_TZ)
.format('HH:mm');
const timeEndString = dayjs
.utc(orderEntity.datetime_end)
.tz(DEFAULT_TZ)
.format('HH:mm');
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 messageForMaster = `${heading}
<b>Дата:</b> ${date}
<b>Время:</b> ${timeStartString} - ${timeEndString}
<b>Клиент:</b> ${clientName}
<b>Услуги:</b> ${servicesList}
<b>Стоимость:</b> ${priceText}
<b>Статус:</b> ${emojiForState} ${stateLabel}`;
const messageForClient = `${heading} if (new Date(datetime_end) <= new Date(datetime_start)) {
<b>Дата:</b> ${date} throw new Error(ERR_INVALID_TIME);
<b>Время:</b> ${timeStartString} - ${timeEndString} }
<b>Мастер:</b> ${masterName}
<b>Услуги:</b> ${servicesList}
<b>Стоимость:</b> ${priceText}
<b>Статус:</b> ${emojiForState} ${stateLabel}`;
if (masterTelegramId) { if (!existingOrder || !existingOrder.slot) {
await bot.telegram.sendMessage(masterTelegramId, messageForMaster, { throw new Error(ERR_EXISTING_ORDER_OR_SLOT_NOT_FOUND);
parse_mode: 'HTML', }
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);
} }
if (clientTelegramId) {
await bot.telegram.sendMessage(clientTelegramId, messageForClient, {
parse_mode: 'HTML',
});
}
} catch (error) {
console.error('❌ Error sending Telegram notification:', error);
}
}
export default {
async afterCreate({ result }) {
const createdEntity = result as Order;
if (!createdEntity.publishedAt) return;
const isUpdate = createdEntity.createdAt !== createdEntity.updatedAt;
await sendTelegramNotification(createdEntity, isUpdate);
},
async afterUpdate({ result, params }) {
const updatedEntity = result as Order;
if (!updatedEntity.publishedAt) return;
const previousState = params?.data?.state;
if (!previousState || previousState === updatedEntity.state) return;
await sendTelegramNotification(updatedEntity, true);
}, },
}; };

View File

@ -24,12 +24,24 @@
"cancelled" "cancelled"
] ]
}, },
"price": {
"type": "integer"
},
"service_description": {
"type": "text"
},
"client": { "client": {
"type": "relation", "type": "relation",
"relation": "manyToOne", "relation": "manyToOne",
"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,9 +13,7 @@
"pluginOptions": {}, "pluginOptions": {},
"attributes": { "attributes": {
"name": { "name": {
"type": "string", "type": "string"
"maxLength": 100,
"required": true
}, },
"orders": { "orders": {
"type": "relation", "type": "relation",
@ -33,17 +31,6 @@
"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

@ -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,151 @@
import { extractId } from '../../../../utils';
const dayjs = require('dayjs');
const utc = require('dayjs/plugin/utc');
const timezone = require('dayjs/plugin/timezone');
if (!dayjs.prototype.tz) {
dayjs.extend(utc);
dayjs.extend(timezone);
}
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('Europe/Moscow');
const slotStart = dayjs(datetime_start).tz('Europe/Moscow');
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

@ -1 +0,0 @@
export const DEFAULT_TZ = 'Europe/Moscow';

View File

@ -9,8 +9,5 @@ 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

@ -1,13 +0,0 @@
import dayjs, { type ConfigType } from 'dayjs';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
import 'dayjs/locale/ru';
if (!dayjs.prototype.tz) {
dayjs.extend(utc);
dayjs.extend(timezone);
}
dayjs.locale('ru');
export { dayjs };

View File

@ -1,3 +0,0 @@
export * from './telegram';
export * from './db';
export * from './datetime';

View File

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

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 { export interface ApiCustomerCustomer extends Struct.CollectionTypeSchema {
collectionName: 'customers'; collectionName: 'customers';
info: { info: {
@ -386,55 +423,29 @@ export interface ApiCustomerCustomer extends Struct.CollectionTypeSchema {
}; };
attributes: { attributes: {
active: Schema.Attribute.Boolean & Schema.Attribute.DefaultTo<false>; 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; 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;
name: Schema.Attribute.String & masters: Schema.Attribute.Relation<'manyToMany', 'api::customer.customer'>;
Schema.Attribute.Required & name: Schema.Attribute.String & 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'> &
@ -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 { export interface ApiOrderOrder extends Struct.CollectionTypeSchema {
collectionName: 'orders'; collectionName: 'orders';
info: { info: {
@ -484,6 +465,7 @@ 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'> &
@ -494,7 +476,9 @@ export interface ApiOrderOrder extends Struct.CollectionTypeSchema {
localizations: Schema.Attribute.Relation<'oneToMany', 'api::order.order'> & localizations: Schema.Attribute.Relation<'oneToMany', 'api::order.order'> &
Schema.Attribute.Private; Schema.Attribute.Private;
order_number: Schema.Attribute.Integer; order_number: Schema.Attribute.Integer;
price: Schema.Attribute.Integer;
publishedAt: Schema.Attribute.DateTime; publishedAt: Schema.Attribute.DateTime;
service_description: Schema.Attribute.Text;
services: Schema.Attribute.Relation<'manyToMany', 'api::service.service'>; services: Schema.Attribute.Relation<'manyToMany', 'api::service.service'>;
slot: Schema.Attribute.Relation<'manyToOne', 'api::slot.slot'>; slot: Schema.Attribute.Relation<'manyToOne', 'api::slot.slot'>;
state: Schema.Attribute.Enumeration< state: Schema.Attribute.Enumeration<
@ -518,7 +502,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';
}; };
@ -526,11 +510,9 @@ 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'>;
@ -541,19 +523,8 @@ 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'> &
@ -561,6 +532,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 { export interface ApiSlotSlot extends Struct.CollectionTypeSchema {
collectionName: 'slots'; collectionName: 'slots';
info: { info: {
@ -591,212 +592,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 export interface PluginContentReleasesRelease
extends Struct.CollectionTypeSchema { extends Struct.CollectionTypeSchema {
collectionName: 'strapi_releases'; collectionName: 'strapi_releases';
@ -1306,16 +1101,12 @@ 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;