Merge branch 'master' into production
This commit is contained in:
@@ -1,148 +0,0 @@
|
||||
# Pager Socket.IO Testing Guide
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Start your NestJS server:**
|
||||
```bash
|
||||
npm run start:dev
|
||||
```
|
||||
|
||||
2. **Open the test page:**
|
||||
- Open `test-pager-socket.html` in your web browser
|
||||
- Or serve it via a simple HTTP server:
|
||||
```bash
|
||||
# Using Python
|
||||
python3 -m http.server 8080
|
||||
# Then open http://localhost:8080/test-pager-socket.html
|
||||
|
||||
# Using Node.js (if you have http-server installed)
|
||||
npx http-server -p 8080
|
||||
```
|
||||
|
||||
## Testing Scenarios
|
||||
|
||||
### Scenario 1: Test as Restaurant Admin/Staff
|
||||
|
||||
1. **Get your admin JWT token:**
|
||||
- Login as admin via your API to get the JWT token
|
||||
- Example:
|
||||
```bash
|
||||
curl -X POST http://localhost:4000/admin/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "admin@example.com",
|
||||
"password": "password"
|
||||
}'
|
||||
```
|
||||
- Copy the `accessToken` from the response
|
||||
|
||||
2. **Connect to the server:**
|
||||
- Enter server URL: `http://localhost:4000`
|
||||
- Enter your admin JWT token in the "Admin Token" field
|
||||
- Click "Connect"
|
||||
|
||||
3. **Join restaurant room:**
|
||||
- Select "Restaurant (Admin/Staff)" from User Type
|
||||
- Click "Join Room"
|
||||
- The restaurant ID will be automatically extracted from your token
|
||||
|
||||
3. **Create a pager request (via API):**
|
||||
```bash
|
||||
curl -X POST http://localhost:4000/public/pager \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Slug: your-restaurant-slug" \
|
||||
-d '{
|
||||
"tableNumber": "5",
|
||||
"message": "Need assistance"
|
||||
}'
|
||||
```
|
||||
|
||||
4. **Observe the event:**
|
||||
- You should see `pager:created` event in the Events Log
|
||||
|
||||
### Scenario 2: Test as Public User
|
||||
|
||||
1. **Create a pager request first (to get cookie ID):**
|
||||
```bash
|
||||
curl -X POST http://localhost:4000/public/pager \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Slug: your-restaurant-slug" \
|
||||
-d '{
|
||||
"tableNumber": "3",
|
||||
"message": "Need water"
|
||||
}' \
|
||||
-c cookies.txt
|
||||
```
|
||||
|
||||
2. **Get the cookie ID:**
|
||||
- Check the response or cookies.txt file for `pagerId` cookie value
|
||||
|
||||
3. **Connect and join session:**
|
||||
- Connect to the server
|
||||
- Select "Session (Public User)" from User Type
|
||||
- Enter the cookie ID from step 2
|
||||
- Click "Join Room"
|
||||
|
||||
4. **Update pager status (via API as admin):**
|
||||
```bash
|
||||
curl -X PATCH http://localhost:4000/admin/pager/{pagerId}/status \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
|
||||
-d '{
|
||||
"status": "acknowledged"
|
||||
}'
|
||||
```
|
||||
|
||||
5. **Observe the event:**
|
||||
- You should see `pager:status:updated` event in the Events Log
|
||||
|
||||
## Socket Events Reference
|
||||
|
||||
### Client → Server Events
|
||||
|
||||
| Event | Payload | Description |
|
||||
|-------|---------|-------------|
|
||||
| `join:restaurant` | `{}` (empty, restId extracted from token) | Join restaurant room for admin/staff. **Requires authentication via JWT token in connection auth** |
|
||||
| `join:session` | `{ cookieId: string }` | Join session room for public user |
|
||||
| `leave:room` | `{ room: string }` | Leave a specific room |
|
||||
|
||||
**Note:** For `join:restaurant`, the restaurant ID is automatically extracted from the authenticated admin's JWT token. You must provide the token when connecting:
|
||||
|
||||
```javascript
|
||||
const socket = io('http://localhost:4000/pager', {
|
||||
auth: {
|
||||
token: 'your-jwt-token-here' // or 'Bearer your-jwt-token-here'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Server → Client Events
|
||||
|
||||
| Event | Payload | Description |
|
||||
|-------|---------|-------------|
|
||||
| `pager:created` | `{ pager: {...} }` | New pager request created |
|
||||
| `pager:status:updated` | `{ pager: {...} }` | Pager status updated |
|
||||
| `joined` | `{ room: string, message: string }` | Successfully joined a room |
|
||||
| `left` | `{ room: string, message: string }` | Successfully left a room |
|
||||
| `error` | `{ message: string }` | Error occurred |
|
||||
|
||||
## Room Names
|
||||
|
||||
- **Restaurant Room:** `restaurant:{restId}`
|
||||
- **Session Room:** `cookie:{cookieId}`
|
||||
|
||||
## Testing Tips
|
||||
|
||||
1. **Open multiple browser tabs/windows** to simulate multiple clients
|
||||
2. **Use browser DevTools** to inspect WebSocket connections
|
||||
3. **Check server logs** to see gateway activity
|
||||
4. **Test reconnection** by disconnecting and reconnecting
|
||||
5. **Test room isolation** by joining different rooms in different tabs
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Connection fails:** Check if server is running on the correct port
|
||||
- **No events received:** Verify you've joined the correct room
|
||||
- **CORS errors:** Ensure CORS is enabled in your NestJS app (already configured in gateway)
|
||||
- **Events not showing:** Check browser console for errors
|
||||
|
||||
Generated
+182
-3
@@ -48,6 +48,7 @@
|
||||
"firebase-admin": "^13.6.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"sanitize-html": "^2.17.0",
|
||||
"socket.io": "^4.8.1",
|
||||
"ulid": "^3.0.1",
|
||||
"uuid": "^13.0.0"
|
||||
@@ -63,6 +64,7 @@
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/sanitize-html": "^2.16.0",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"eslint": "^9.18.0",
|
||||
@@ -6476,6 +6478,16 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/sanitize-html": {
|
||||
"version": "2.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.16.0.tgz",
|
||||
"integrity": "sha512-l6rX1MUXje5ztPT0cAFtUayXF06DqPhRyfVXareEN5gGCFaP/iwsxIyKODr9XDhfxPpN6vXUFNfo5kZMXCxBtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"htmlparser2": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/send": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.0.tgz",
|
||||
@@ -8712,7 +8724,6 @@
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -8837,6 +8848,61 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"entities": "^4.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domelementtype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/domhandler": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.2.3",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
|
||||
@@ -9081,6 +9147,18 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/error-ex": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
|
||||
@@ -9162,7 +9240,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -10786,6 +10863,25 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/htmlparser2": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||
"funding": [
|
||||
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.0.1",
|
||||
"entities": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
@@ -11113,6 +11209,15 @@
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-object": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
||||
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||
@@ -13011,6 +13116,24 @@
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/napi-postinstall": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
|
||||
@@ -13377,6 +13500,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-srcset": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
|
||||
"integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
@@ -13594,7 +13723,6 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
@@ -13745,6 +13873,34 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz",
|
||||
@@ -14371,6 +14527,20 @@
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sanitize-html": {
|
||||
"version": "2.17.0",
|
||||
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.0.tgz",
|
||||
"integrity": "sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"deepmerge": "^4.2.2",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"htmlparser2": "^8.0.0",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"parse-srcset": "^1.0.2",
|
||||
"postcss": "^8.3.11"
|
||||
}
|
||||
},
|
||||
"node_modules/schema-utils": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
|
||||
@@ -14782,6 +14952,15 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-support": {
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
"firebase-admin": "^13.6.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"sanitize-html": "^2.17.0",
|
||||
"socket.io": "^4.8.1",
|
||||
"ulid": "^3.0.1",
|
||||
"uuid": "^13.0.0"
|
||||
@@ -79,6 +80,7 @@
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/sanitize-html": "^2.16.0",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"eslint": "^9.18.0",
|
||||
|
||||
@@ -40,7 +40,7 @@ export class AdminRepository extends EntityRepository<Admin> {
|
||||
// Ensure all roles are populated
|
||||
await adminRole.admin.roles.loadItems();
|
||||
for (const role of adminRole.admin.roles.getItems()) {
|
||||
if (role.role && !role.role.permissions.isInitialized()) {
|
||||
if (role.role && role.role.permissions && !role.role.permissions.isInitialized()) {
|
||||
await role.role.permissions.loadItems();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export class CartValidationService {
|
||||
validateStock(food: Food, quantity: number): void {
|
||||
const availableStock = food.inventory!.availableStock;
|
||||
if (availableStock < quantity) {
|
||||
throw new BadRequestException(CartMessage.INSUFFICIENT_STOCK);
|
||||
throw new BadRequestException(CartMessage.INSUFFICIENT_STOCK + food.title);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -155,9 +155,9 @@ export class NotificationsController {
|
||||
return this.preferenceService.updatePreference(preferenceId, restaurantId, dto);
|
||||
}
|
||||
|
||||
@UseGuards(SuperAdminAuthGuard)
|
||||
// @UseGuards(SuperAdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Get('super-admin/sms-count')
|
||||
@Get('super-admin/notifications/sms-count')
|
||||
@ApiOperation({ summary: 'Get SMS count for each restaurant with pagination' })
|
||||
@ApiQuery({ name: 'page', required: false, type: Number, description: 'Page number (default: 1)', minimum: 1 })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number, description: 'Items per page (default: 10)', minimum: 1 })
|
||||
|
||||
@@ -5,15 +5,19 @@ import { PagerCreatedEvent } from '../events/pager.events';
|
||||
import { Permission } from 'src/common/enums/permission.enum';
|
||||
import { NotifTitleEnum } from 'src/modules/notifications/interfaces/notification.interface';
|
||||
import { NotificationService } from 'src/modules/notifications/services/notification.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class PagerListeners {
|
||||
private readonly logger = new Logger(PagerListeners.name);
|
||||
|
||||
private readonly smsPatternPagerCreated: string
|
||||
constructor(
|
||||
private readonly adminService: AdminRepository,
|
||||
private readonly notificationService: NotificationService,
|
||||
) {}
|
||||
private readonly configService: ConfigService,
|
||||
) {
|
||||
this.smsPatternPagerCreated = this.configService.get<string>('SMS_PATTERN_PAGER_CREATED') ?? '123';
|
||||
}
|
||||
|
||||
@OnEvent(PagerCreatedEvent.name)
|
||||
async handlePagerCreated(event: PagerCreatedEvent) {
|
||||
@@ -30,7 +34,7 @@ export class PagerListeners {
|
||||
title: NotifTitleEnum.PAGER_CREATED,
|
||||
content: `میز شماره ${event.tableNumber} پیج جدیدی انجام داد`,
|
||||
sms: {
|
||||
templateId: '1234567890',
|
||||
templateId: this.smsPatternPagerCreated,
|
||||
parameters: {
|
||||
tableNumber: event.tableNumber.toString(),
|
||||
},
|
||||
|
||||
@@ -45,7 +45,7 @@ export class PaymentsController {
|
||||
@ApiOperation({ summary: 'Get all the restaurant payments for user' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
findAllByRestaurant(@UserId() userId: string, @RestId() restId: string) {
|
||||
return this.paymentsService.findAllByRestaurantId(restId, userId);
|
||||
return this.paymentsService.findAllPaymentsByRestaurantId(restId, userId);
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
|
||||
@@ -219,7 +219,7 @@ export class PaymentsService {
|
||||
return payment;
|
||||
}
|
||||
|
||||
findAllByRestaurantId(restId: string, userId: string) {
|
||||
findAllPaymentsByRestaurantId(restId: string, userId: string) {
|
||||
return this.em.find(
|
||||
Payment,
|
||||
{ order: { restaurant: { id: restId }, user: { id: userId } } },
|
||||
|
||||
@@ -77,7 +77,16 @@ export class RestaurantsController {
|
||||
@ApiOperation({ summary: 'Create a new restaurant' })
|
||||
@Post('super-admin/restaurants')
|
||||
createRestaurant(@Body() createRestaurantDto: CreateRestaurantDto) {
|
||||
return this.restaurantsService.create(createRestaurantDto);
|
||||
return this.restaurantsService.setupRestuarant(createRestaurantDto);
|
||||
}
|
||||
|
||||
@UseGuards(SuperAdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Get restaurant by subscription ID' })
|
||||
@ApiParam({ name: 'subscriptionId', required: true, description: 'Subscription ID' })
|
||||
@Get('super-admin/restaurants/subscription/:subscriptionId')
|
||||
findOneBySubscriptionId(@Param('subscriptionId') subscriptionId: string) {
|
||||
return this.restaurantsService.findOneBySubscriptionId(subscriptionId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { Restaurant } from '../entities/restaurant.entity';
|
||||
|
||||
@Injectable()
|
||||
export class RestaurantCrone {
|
||||
private readonly logger = new Logger(RestaurantCrone.name);
|
||||
|
||||
constructor(private readonly em: EntityManager) { }
|
||||
|
||||
// run every day at 03:00 (3:00 AM)
|
||||
@Cron('0 3 * * *', {
|
||||
name: 'deactivateExpiredSubscriptions',
|
||||
timeZone: 'UTC',
|
||||
})
|
||||
async handleCron() {
|
||||
try {
|
||||
this.logger.debug('Starting daily subscription expiration check');
|
||||
|
||||
// Get current date
|
||||
const now = new Date();
|
||||
|
||||
// Find restaurants where subscription has expired and they're still active
|
||||
const expiredRestaurants = await this.em.find(Restaurant, {
|
||||
subscriptionEndDate: { $lt: now },
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
if (!expiredRestaurants || expiredRestaurants.length === 0) {
|
||||
this.logger.debug('No restaurants with expired subscriptions found');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(`Found ${expiredRestaurants.length} restaurants with expired subscriptions`);
|
||||
|
||||
// Deactivate expired restaurants
|
||||
for (const restaurant of expiredRestaurants) {
|
||||
restaurant.isActive = false;
|
||||
}
|
||||
|
||||
// Persist all changes
|
||||
await this.em.persistAndFlush(expiredRestaurants);
|
||||
|
||||
this.logger.log(`Successfully deactivated ${expiredRestaurants.length} restaurants with expired subscriptions`);
|
||||
} catch (err) {
|
||||
this.logger.error(`RestaurantCrone failed: ${err?.message}`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsString, IsOptional, IsNumber } from 'class-validator';
|
||||
import { IsString, IsOptional, IsNumber, IsDate, IsEnum } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { PlanEnum } from '../interface/plan.interface';
|
||||
|
||||
export class CreateRestaurantDto {
|
||||
@ApiProperty({ example: 'کافه ژیوان', description: 'نام رستوران' })
|
||||
@@ -13,15 +15,29 @@ export class CreateRestaurantDto {
|
||||
@ApiPropertyOptional({ example: 2020, description: 'سال تأسیس' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
establishedYear?: number;
|
||||
establishedYear: number;
|
||||
|
||||
@ApiPropertyOptional({ example: '09123456789', description: 'شماره تلفن' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
phone: string;
|
||||
|
||||
@ApiProperty({ example: 'base', enum: PlanEnum, description: 'پلن رستوران' })
|
||||
@IsEnum(PlanEnum)
|
||||
plan!: PlanEnum;
|
||||
|
||||
@ApiProperty({ example: 'sub_1234567890', description: 'شناسه اشتراک' })
|
||||
@IsString()
|
||||
subscriptionId!: string;
|
||||
|
||||
@ApiProperty({ example: '2025-12-26', description: 'تاریخ انقضای اشتراک' })
|
||||
@Type(() => Date)
|
||||
@IsDate()
|
||||
subscriptionEndDate!: Date;
|
||||
|
||||
@ApiProperty({ example: '2025-12-26', description: 'تاریخ شروع اشتراک' })
|
||||
@Type(() => Date)
|
||||
@IsDate()
|
||||
subscriptionStartDate!: Date;
|
||||
|
||||
}
|
||||
|
||||
@@ -102,4 +102,11 @@ export class Restaurant extends BaseEntity {
|
||||
|
||||
@Property({unique: true})
|
||||
subscriptionId!: string;
|
||||
|
||||
@Property()
|
||||
subscriptionEndDate!: Date;
|
||||
|
||||
@Property()
|
||||
subscriptionStartDate!: Date;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { CreateRestaurantDto } from '../dto/create-restaurant.dto';
|
||||
import { UpdateRestaurantDto } from '../dto/update-restaurant.dto';
|
||||
import { Restaurant } from '../entities/restaurant.entity';
|
||||
@@ -8,6 +8,10 @@ import { RestMessage } from 'src/common/enums/message.enum';
|
||||
import { FindRestaurantsDto } from '../dto/find-restaurants.dto';
|
||||
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
||||
import { PlanEnum } from '../interface/plan.interface';
|
||||
import { Admin } from '../../admin/entities/admin.entity';
|
||||
import { AdminRole } from '../../admin/entities/adminRole.entity';
|
||||
import { Role } from '../../roles/entities/role.entity';
|
||||
import { normalizePhone } from 'src/modules/utils/phone.util';
|
||||
|
||||
@Injectable()
|
||||
export class RestaurantsService {
|
||||
@@ -16,20 +20,62 @@ export class RestaurantsService {
|
||||
private readonly restRepository: RestRepository,
|
||||
) { }
|
||||
|
||||
async create(dto: CreateRestaurantDto): Promise<Restaurant> {
|
||||
const restaurant = this.em.create(Restaurant, {
|
||||
name: dto.name,
|
||||
slug: dto.slug,
|
||||
establishedYear: dto.establishedYear,
|
||||
phone: dto.phone,
|
||||
isActive: true,
|
||||
plan: PlanEnum.Base,
|
||||
domain: `https://dmenu-plus-front.dev.danakcorp.com/${dto.slug}`,
|
||||
subscriptionId: dto.subscriptionId,
|
||||
});
|
||||
async setupRestuarant(dto: CreateRestaurantDto): Promise<Restaurant> {
|
||||
return await this.em.transactional(async (em) => {
|
||||
// Validate unique constraints
|
||||
const validateSlug = await em.findOne(Restaurant, { slug: dto.slug })
|
||||
if (validateSlug) {
|
||||
throw new BadRequestException("Duplicate slug: " + dto.slug)
|
||||
}
|
||||
const validateSubscriptionId = await em.findOne(Restaurant, { subscriptionId: dto.subscriptionId })
|
||||
if (validateSubscriptionId) {
|
||||
throw new BadRequestException("Duplicate subscriptionId: " + dto.subscriptionId)
|
||||
}
|
||||
|
||||
await this.em.persistAndFlush(restaurant);
|
||||
return restaurant;
|
||||
// Create restaurant
|
||||
const restaurant = em.create(Restaurant, {
|
||||
name: dto.name,
|
||||
slug: dto.slug,
|
||||
establishedYear: dto.establishedYear,
|
||||
phone: dto.phone,
|
||||
isActive: true,
|
||||
plan: dto.plan,
|
||||
domain: `https://dmenu-plus-front.dev.danakcorp.com/${dto.slug}`,
|
||||
subscriptionId: dto.subscriptionId,
|
||||
subscriptionEndDate: dto.subscriptionEndDate,
|
||||
subscriptionStartDate: dto.subscriptionStartDate,
|
||||
});
|
||||
|
||||
// Find the appropriate role based on plan
|
||||
const roleName = dto.plan === PlanEnum.Base ? 'مدیر (پلن پایه)' : 'مدیر ( پلن ویژه)';
|
||||
const role = await em.findOne(Role, { name: roleName });
|
||||
if (!role) {
|
||||
throw new BadRequestException(`Role not found for plan: ${dto.plan}`);
|
||||
}
|
||||
|
||||
const normalizedPhone = normalizePhone(dto.phone);
|
||||
let admin = await em.findOne(Admin, { phone: normalizedPhone });
|
||||
if (!admin) {
|
||||
admin = em.create(Admin, {
|
||||
phone: normalizedPhone,
|
||||
firstName: 'نام مدیر',
|
||||
lastName: 'نام خانوادگی مدیر',
|
||||
});
|
||||
}
|
||||
|
||||
// Create admin role relationship
|
||||
const adminRole = em.create(AdminRole, {
|
||||
admin,
|
||||
role,
|
||||
restaurant,
|
||||
});
|
||||
|
||||
// Persist all entities
|
||||
em.persist([restaurant, admin, adminRole]);
|
||||
await em.flush();
|
||||
|
||||
return restaurant;
|
||||
});
|
||||
}
|
||||
|
||||
async findAll(dto: FindRestaurantsDto): Promise<PaginatedResult<Restaurant>> {
|
||||
@@ -64,6 +110,14 @@ export class RestaurantsService {
|
||||
return restaurant;
|
||||
}
|
||||
|
||||
async findOneBySubscriptionId(subscriptionId: string): Promise<Restaurant> {
|
||||
const restaurant = await this.restRepository.findOne({ subscriptionId });
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
return restaurant;
|
||||
}
|
||||
|
||||
async getRestaurantSpecification(slug: string) {
|
||||
const restaurant = await this.findBySlug(slug);
|
||||
return restaurant;
|
||||
|
||||
@@ -8,12 +8,13 @@ import { ScheduleRepository } from './repositories/schedule.repository';
|
||||
import { Schedule } from './entities/schedule.entity';
|
||||
import { ScheduleService } from './providers/schedule.service';
|
||||
import { ScheduleController } from './controllers/schedule.controller';
|
||||
import { RestaurantCrone } from './crone/restaurant.crone';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
|
||||
@Module({
|
||||
controllers: [RestaurantsController, ScheduleController],
|
||||
providers: [RestaurantsService, RestRepository, ScheduleRepository, ScheduleService],
|
||||
providers: [RestaurantsService, RestRepository, ScheduleRepository, ScheduleService, RestaurantCrone],
|
||||
imports: [MikroOrmModule.forFeature([Restaurant, Schedule]), JwtModule, forwardRef(() => AuthModule)],
|
||||
exports: [RestRepository, ScheduleRepository, ScheduleService],
|
||||
})
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export class ReviewCreatedEvent {
|
||||
constructor(
|
||||
public readonly reviewId: string,
|
||||
public readonly restaurantId: string,
|
||||
public readonly userId: string,
|
||||
public readonly foodId: string,
|
||||
public readonly foodName: string,
|
||||
public readonly orderId: string,
|
||||
public readonly orderNumber: string | number,
|
||||
public readonly rating: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { ReviewCreatedEvent } from '../events/review.events';
|
||||
import { AdminRepository } from 'src/modules/admin/repositories/admin.repository';
|
||||
import { Permission } from 'src/common/enums/permission.enum';
|
||||
import { NotifTitleEnum } from 'src/modules/notifications/interfaces/notification.interface';
|
||||
import { NotificationService } from 'src/modules/notifications/services/notification.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class ReviewListeners {
|
||||
private readonly logger = new Logger(ReviewListeners.name);
|
||||
private reviewCreatedSmsTemplateId: string;
|
||||
|
||||
constructor(
|
||||
private readonly adminService: AdminRepository,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly configService: ConfigService,
|
||||
) {
|
||||
this.reviewCreatedSmsTemplateId = this.configService.get<string>('SMS_PATTERN_REVIEW_CREATED') ?? '123';
|
||||
}
|
||||
|
||||
@OnEvent(ReviewCreatedEvent.name)
|
||||
async handleReviewCreated(event: ReviewCreatedEvent) {
|
||||
try {
|
||||
this.logger.log(
|
||||
`Review created event received: ${event.reviewId} for restaurant: ${event.restaurantId}, food: ${event.foodName}, rating: ${event.rating}`,
|
||||
);
|
||||
|
||||
// Get admins of restaurant that have review management permissions
|
||||
const admins = await this.adminService.findAdminsWithPermission(event.restaurantId, Permission.MANAGE_REVIEWS);
|
||||
const recipients = admins.map(admin => ({
|
||||
adminId: admin.id,
|
||||
}));
|
||||
|
||||
if (recipients.length === 0) {
|
||||
this.logger.warn(`No admins found with MANAGE_REVIEWS permission for restaurant ${event.restaurantId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.notificationService.sendNotification({
|
||||
restaurantId: event.restaurantId,
|
||||
message: {
|
||||
title: NotifTitleEnum.REVIEW_CREATED,
|
||||
content: `نظر جدید برای غذا "${event.foodName}" با امتیاز ${event.rating} از 5 برای سفارش شماره ${event.orderNumber} ثبت شد`,
|
||||
sms: {
|
||||
templateId: this.reviewCreatedSmsTemplateId,
|
||||
parameters: {
|
||||
foodName: event.foodName,
|
||||
rating: event.rating.toString(),
|
||||
orderNumber: String(event.orderNumber),
|
||||
},
|
||||
},
|
||||
pushNotif: {
|
||||
title: `نظر جدید`,
|
||||
content: `نظر جدید برای غذا "${event.foodName}" با امتیاز ${event.rating} ثبت شد`,
|
||||
icon: `/`,
|
||||
action: {
|
||||
type: NotifTitleEnum.REVIEW_CREATED,
|
||||
url: `/`,
|
||||
},
|
||||
},
|
||||
},
|
||||
recipients,
|
||||
metadata: {
|
||||
priority: 1,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to send notification for review created event: ${event.reviewId}`,
|
||||
error instanceof Error ? error.stack : String(error),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ import { Order } from '../../orders/entities/order.entity';
|
||||
import { OrderItem } from '../../orders/entities/order-item.entity';
|
||||
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
|
||||
import { ReviewStatus } from '../enums/review-status.enum';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { ReviewCreatedEvent } from '../events/review.events';
|
||||
import { sanitizeText } from '../../utils/sanitize.util';
|
||||
|
||||
@Injectable()
|
||||
export class ReviewService {
|
||||
@@ -21,6 +24,7 @@ export class ReviewService {
|
||||
private readonly foodRepository: FoodRepository,
|
||||
private readonly userRepository: UserRepository,
|
||||
private readonly em: EntityManager,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
) { }
|
||||
|
||||
async create(userId: string, createReviewDto: CreateReviewDto): Promise<Review> {
|
||||
@@ -36,8 +40,8 @@ export class ReviewService {
|
||||
throw new NotFoundException(UserMessage.USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Find order and verify it belongs to the user
|
||||
const order = await this.em.findOne(Order, { id: orderId, user: { id: userId } });
|
||||
// Find order and verify it belongs to the user, populate restaurant to get restaurantId
|
||||
const order = await this.em.findOne(Order, { id: orderId, user: { id: userId } }, { populate: ['restaurant'] });
|
||||
if (!order) {
|
||||
throw new NotFoundException('Order not found or does not belong to the current user');
|
||||
}
|
||||
@@ -67,7 +71,7 @@ export class ReviewService {
|
||||
food,
|
||||
user,
|
||||
rating,
|
||||
comment: comment || undefined,
|
||||
comment: sanitizeText(comment) || undefined,
|
||||
positivePoints: positivePoints || undefined,
|
||||
negativePoints: negativePoints || undefined,
|
||||
status: ReviewStatus.PENDING,
|
||||
@@ -83,6 +87,21 @@ export class ReviewService {
|
||||
// Update food rating average
|
||||
await this.updateFoodRating(foodId);
|
||||
|
||||
// Emit ReviewCreatedEvent
|
||||
this.eventEmitter.emit(
|
||||
ReviewCreatedEvent.name,
|
||||
new ReviewCreatedEvent(
|
||||
review.id,
|
||||
order.restaurant.id,
|
||||
userId,
|
||||
foodId,
|
||||
food.title || 'Unknown Food',
|
||||
orderId,
|
||||
order.orderNumber || '',
|
||||
rating,
|
||||
),
|
||||
);
|
||||
|
||||
return review;
|
||||
}
|
||||
|
||||
@@ -127,7 +146,14 @@ export class ReviewService {
|
||||
}
|
||||
|
||||
const oldRating = review.rating;
|
||||
this.em.assign(review, dto);
|
||||
|
||||
// Sanitize comment if provided
|
||||
const sanitizedDto = {
|
||||
...dto,
|
||||
comment: dto.comment !== undefined ? sanitizeText(dto.comment) || undefined : undefined,
|
||||
};
|
||||
|
||||
this.em.assign(review, sanitizedDto);
|
||||
await this.em.persistAndFlush(review);
|
||||
|
||||
// Update food rating average if rating changed
|
||||
|
||||
@@ -9,11 +9,14 @@ import { FoodModule } from '../foods/food.module';
|
||||
import { UserModule } from '../users/user.module';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { ReviewListeners } from './listeners/review.listeners';
|
||||
import { AdminModule } from '../admin/admin.module';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
|
||||
@Module({
|
||||
imports: [MikroOrmModule.forFeature([Review]), FoodModule, UserModule, AuthModule, JwtModule],
|
||||
imports: [MikroOrmModule.forFeature([Review]), FoodModule, UserModule, AuthModule, JwtModule, AdminModule, NotificationsModule],
|
||||
controllers: [ReviewController],
|
||||
providers: [ReviewService, ReviewRepository, FoodRatingCronService],
|
||||
providers: [ReviewService, ReviewRepository, FoodRatingCronService, ReviewListeners],
|
||||
exports: [ReviewRepository],
|
||||
})
|
||||
export class ReviewModule {}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@mikro-orm/nestjs';
|
||||
import { EntityRepository } from '@mikro-orm/core';
|
||||
import { EntityManager, EntityRepository } from '@mikro-orm/core';
|
||||
import { Permission } from '../entities/permission.entity';
|
||||
import { CacheService } from 'src/modules/utils/cache.service';
|
||||
import { AdminRepository } from 'src/modules/admin/repositories/admin.repository';
|
||||
import { AdminRole } from 'src/modules/admin/entities/adminRole.entity';
|
||||
|
||||
@Injectable()
|
||||
export class PermissionsService {
|
||||
@@ -14,7 +15,8 @@ export class PermissionsService {
|
||||
private readonly permissionRepository: EntityRepository<Permission>,
|
||||
private readonly cacheService: CacheService,
|
||||
private readonly adminRepository: AdminRepository,
|
||||
) {}
|
||||
private readonly em: EntityManager,
|
||||
) { }
|
||||
|
||||
async findAll() {
|
||||
const permissions = await this.permissionRepository.findAll();
|
||||
@@ -76,33 +78,19 @@ export class PermissionsService {
|
||||
}
|
||||
|
||||
async getAdminFullPermissions(adminId: string, restId: string): Promise<Permission[]> {
|
||||
const admin = await this.adminRepository.findOne(
|
||||
{ id: adminId, roles: { restaurant: { id: restId } } },
|
||||
{ populate: ['roles', 'roles.role', 'roles.role.permissions'] },
|
||||
);
|
||||
|
||||
if (!admin || !admin.roles) {
|
||||
return [];
|
||||
const listOfPermissions = []
|
||||
const adminRoles = await this.em.findOne(AdminRole, {
|
||||
admin: {
|
||||
id: adminId,
|
||||
},
|
||||
restaurant: {
|
||||
id: restId,
|
||||
},
|
||||
}, { populate: ['role', 'role.permissions'] });
|
||||
if (adminRoles) {
|
||||
listOfPermissions.push(...adminRoles.role.permissions.getItems());
|
||||
}
|
||||
|
||||
// Ensure roles collection is loaded
|
||||
await admin.roles.loadItems();
|
||||
|
||||
// Extract permission names as array of strings
|
||||
const permissions = await Promise.all(
|
||||
admin.roles
|
||||
.getItems()
|
||||
.filter(r => r.role) // Filter out any null/undefined roles
|
||||
.map(async r => {
|
||||
// Ensure permissions collection is initialized
|
||||
if (!r.role.permissions.isInitialized()) {
|
||||
await r.role.permissions.loadItems();
|
||||
}
|
||||
return r.role.permissions.getItems();
|
||||
}),
|
||||
);
|
||||
|
||||
return permissions.flat();
|
||||
return listOfPermissions.map(permission => permission);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
|
||||
/**
|
||||
* Sanitizes user input to prevent XSS attacks and remove malicious content
|
||||
* Allows only safe text content, removing all HTML tags and scripts
|
||||
* @param input - The string to sanitize
|
||||
* @returns Sanitized string with all HTML tags and scripts removed
|
||||
*/
|
||||
export function sanitizeText(input: string | undefined | null): string | undefined | null {
|
||||
if (!input) {
|
||||
return input;
|
||||
}
|
||||
|
||||
return sanitizeHtml(input, {
|
||||
allowedTags: [], // No HTML tags allowed
|
||||
allowedAttributes: {}, // No attributes allowed
|
||||
allowedSchemes: [], // No URL schemes allowed
|
||||
}).trim();
|
||||
}
|
||||
|
||||
@@ -11,14 +11,14 @@ export const adminsData: AdminData[] = [
|
||||
phone: '09362532122',
|
||||
firstName: 'مرتضی',
|
||||
lastName: 'مرتضایی',
|
||||
roleName: 'restaurant',
|
||||
roleName: 'مدیر ( پلن ویژه)',
|
||||
restaurantSlug: 'zhivan',
|
||||
},
|
||||
{
|
||||
phone: '09185290775',
|
||||
firstName: 'حمید',
|
||||
lastName: 'ضرقامی',
|
||||
roleName: 'restaurant',
|
||||
roleName: 'مدیر (پلن پایه)',
|
||||
restaurantSlug: 'boote',
|
||||
},
|
||||
{
|
||||
@@ -32,7 +32,7 @@ export const adminsData: AdminData[] = [
|
||||
phone: '09184317567',
|
||||
firstName: 'ادمین',
|
||||
lastName: 'هنر',
|
||||
roleName: 'restaurant',
|
||||
roleName: 'مدیر (پلن پایه)',
|
||||
restaurantSlug: 'honar',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -20,7 +20,7 @@ export interface RestaurantData {
|
||||
marriageDateScore: string;
|
||||
referrerScore: string;
|
||||
};
|
||||
plan: PlanEnum.Premium;
|
||||
plan: PlanEnum;
|
||||
subscriptionId: string;
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export const restaurantsData: RestaurantData[] = [
|
||||
marriageDateScore: '10000',
|
||||
referrerScore: '10000',
|
||||
},
|
||||
plan: PlanEnum.Premium,
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_boote_001',
|
||||
},
|
||||
{
|
||||
@@ -115,7 +115,7 @@ export const restaurantsData: RestaurantData[] = [
|
||||
marriageDateScore: '10000',
|
||||
referrerScore: '10000',
|
||||
},
|
||||
plan: PlanEnum.Premium,
|
||||
plan: PlanEnum.Base,
|
||||
subscriptionId: 'sub_seed_honar_001',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -8,12 +8,24 @@ export interface RoleConfig {
|
||||
|
||||
export const rolesData: RoleConfig[] = [
|
||||
{
|
||||
name: 'restaurant',
|
||||
name: 'مدیر (پلن پایه)',
|
||||
isSystem: false,
|
||||
permissionFilter: (name: Permission) =>
|
||||
name === Permission.MANAGE_FOODS ||
|
||||
name === Permission.MANAGE_CATEGORIES ||
|
||||
name === Permission.MANAGE_SCHEDULES ||
|
||||
name === Permission.MANAGE_PAGER ||
|
||||
name === Permission.MANAGE_CONTACTS ||
|
||||
name === Permission.MANAGE_ROLES ||
|
||||
name === Permission.MANAGE_SETTINGS
|
||||
},
|
||||
{
|
||||
name: 'مدیر ( پلن ویژه)',
|
||||
isSystem: false,
|
||||
permissionFilter: () => true, // All permissions for restaurant role
|
||||
},
|
||||
{
|
||||
name: 'accountant',
|
||||
name: 'حسابدار ',
|
||||
isSystem: false,
|
||||
permissionFilter: (name: Permission) =>
|
||||
name === Permission.VIEW_REPORTS || name === Permission.MANAGE_PAYMENTS || name === Permission.MANAGE_ORDERS,
|
||||
|
||||
@@ -9,7 +9,11 @@ export class RestaurantsSeeder {
|
||||
for (const restaurantData of restaurantsData) {
|
||||
let restaurant = await em.findOne(Restaurant, { slug: restaurantData.slug });
|
||||
if (!restaurant) {
|
||||
restaurant = em.create(Restaurant, restaurantData);
|
||||
restaurant = em.create(Restaurant, {
|
||||
...restaurantData,
|
||||
subscriptionEndDate: new Date('2025-12-26'),
|
||||
subscriptionStartDate: new Date('2026-12-26'),
|
||||
});
|
||||
em.persist(restaurant);
|
||||
}
|
||||
restaurantsMap.set(restaurantData.slug, restaurant);
|
||||
|
||||
@@ -13,7 +13,7 @@ export class RolesSeeder {
|
||||
if (!role) {
|
||||
role = em.create(Role, {
|
||||
name: roleConfig.name,
|
||||
isSystem: roleConfig.isSystem,
|
||||
isSystem: true,
|
||||
});
|
||||
|
||||
// Add permissions based on role configuration
|
||||
|
||||
Reference in New Issue
Block a user