diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml new file mode 100644 index 0000000..a112418 --- /dev/null +++ b/.github/workflows/docker-build-push.yml @@ -0,0 +1,41 @@ +name: Build and Push to Docker Hub + +on: + push: + branches: + - develop + - master + +jobs: + build-and-push: + runs-on: ubuntu-latest + environment: docker + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set Docker tag + id: set-tag + run: | + if [[ ${{ github.ref }} == 'refs/heads/master' ]]; then + echo "DOCKER_TAG=latest" >> $GITHUB_OUTPUT + else + echo "DOCKER_TAG=develop" >> $GITHUB_OUTPUT + fi + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + push: true + tags: akbarsaputrait/ordero-api:${{ steps.set-tag.outputs.DOCKER_TAG }} diff --git a/.vscode/settings.json b/.vscode/settings.json index b715707..09c4570 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,8 @@ { - "[yaml]": { - "editor.defaultFormatter": "redhat.vscode-yaml", - "editor.defaultFoldingRangeProvider": "redhat.vscode-yaml" - } + "editor.codeActionsOnSave": { + "source.fixAll": "always", + "source.organizeImports": "always" + }, + "editor.tabSize": 2, + "editor.indentSize": "tabSize" } \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 543f89b..76a2621 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:16.13.1-alpine AS BUILD_IMAGE +FROM node:16.14.0-alpine AS build_image # couchbase sdk requirements RUN apk update && \ @@ -20,7 +20,7 @@ RUN yarn lint # build application RUN yarn build -FROM node:16.13.1-alpine +FROM node:16.14.0-alpine # Python RUN apk update && \ @@ -34,7 +34,7 @@ RUN apk update && \ # Timezone RUN apk add --no-cache tzdata -ENV TZ UTC +ENV TZ=UTC # Installs latest Chromium (77) package. RUN apk add --no-cache \ @@ -47,13 +47,13 @@ RUN apk add --no-cache \ ttf-freefont # Tell Puppeteer to skip installing Chrome. We'll be using the installed package. -ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true -ENV PUPPETEER_EXECUTABLE_PATH /usr/bin/chromium-browser +ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true +ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser WORKDIR /usr/src/app # copy from build image -COPY --from=BUILD_IMAGE /usr/src/app ./ +COPY --from=build_image /usr/src/app ./ EXPOSE 3000 diff --git a/docker-compose.yaml b/docker-compose.yaml index 0ea5217..8adb43b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -53,7 +53,7 @@ services: api-local: container_name: ordero-api-local - image: 'ordero-api-local:latest' + image: 'ordero-api-local:develop' restart: on-failure environment: TZ: '${TZ}' @@ -91,14 +91,18 @@ services: AWS_REGION: '${AWS_REGION}' AWS_SECRET_ACCESS_KEY: '${AWS_SECRET_ACCESS_KEY}' SENTRY_DSN: '${SENTRY_DSN}' + SOCKET_TYPE: '${SOCKET_TYPE}' + TWILLIO_SID: '${TWILLIO_SID}' + TWILLIO_TOKEN: '${TWILLIO_TOKEN}' + TWILLIO_SERVICE: '${TWILLIO_SERVICE}' volumes: - 'ordero:/api' ports: - - '4002:3000' + - '${PORT_LOCAL}:3000' expose: - '3000' networks: - net: + order_net: ipv4_address: '${NETWORK_IP_LOCAL}' volumes: @@ -107,5 +111,5 @@ volumes: driver: local networks: - net: + order_net: external: true \ No newline at end of file diff --git a/package.json b/package.json index d51f4d9..61c058f 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,9 @@ "aws-sdk": "^2.817.0", "axios": "^1.6.7", "bcryptjs": "^2.4.3", + "bwip-js": "^3.0.2", "class-transformer": "0.3.2", + "convert-html-to-pdf": "^1.0.1", "dayjs": "^1.11.4", "dotenv": "^16.0.1", "express-useragent": "^1.0.15", @@ -63,7 +65,7 @@ "libphonenumber-js": "^1.10.12", "lodash": "^4.17.21", "mysql-import": "^5.0.21", - "mysql2": "^2.3.3", + "mysql2": "^3.10.2", "nest-router": "^1.0.9", "nest-winston": "^1.7.0", "node-cache": "^5.1.2", @@ -80,7 +82,7 @@ "simple-encryptor": "^4.0.0", "slug": "^8.2.3", "twilio": "^3.54.1", - "typeorm": "^0.3.7", + "typeorm": "^0.3.20", "ulid": "^2.3.0", "uuid": "^8.3.2", "validatorjs": "^3.22.1", diff --git a/public/files/1715363520-tables-label.pdf b/public/files/1715363520-tables-label.pdf new file mode 100644 index 0000000..f4005e2 Binary files /dev/null and b/public/files/1715363520-tables-label.pdf differ diff --git a/public/files/1715363558-tables-label.pdf b/public/files/1715363558-tables-label.pdf new file mode 100644 index 0000000..b74068c Binary files /dev/null and b/public/files/1715363558-tables-label.pdf differ diff --git a/public/files/1715363617-tables-label.pdf b/public/files/1715363617-tables-label.pdf new file mode 100644 index 0000000..b5d3df1 Binary files /dev/null and b/public/files/1715363617-tables-label.pdf differ diff --git a/public/files/1715363683-tables-label.pdf b/public/files/1715363683-tables-label.pdf new file mode 100644 index 0000000..8091080 Binary files /dev/null and b/public/files/1715363683-tables-label.pdf differ diff --git a/public/files/1715363705-tables-label.pdf b/public/files/1715363705-tables-label.pdf new file mode 100644 index 0000000..a1dcb7e Binary files /dev/null and b/public/files/1715363705-tables-label.pdf differ diff --git a/public/files/1715363753-tables-label.pdf b/public/files/1715363753-tables-label.pdf new file mode 100644 index 0000000..e3f1ba8 Binary files /dev/null and b/public/files/1715363753-tables-label.pdf differ diff --git a/public/files/1715363799-tables-label.pdf b/public/files/1715363799-tables-label.pdf new file mode 100644 index 0000000..9858bbe Binary files /dev/null and b/public/files/1715363799-tables-label.pdf differ diff --git a/src/app.controller.ts b/src/app.controller.ts index d0f18e7..5c1df4e 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,4 +1,10 @@ +import { Quero } from '@core/decorators/quero.decorator'; +import base64url from '@lib/helpers/base64.helper'; +import { config } from '@lib/helpers/config.helper'; import { Controller, Get, Res } from '@nestjs/common'; +import * as fs from 'fs'; +import { lookup } from 'mime-types'; +import * as path from 'path'; @Controller() export class AppController { @@ -11,4 +17,36 @@ export class AppController { favicon(@Res() response): string { return response.noContent(); } + + @Get('/files') + getFiles(@Quero() quero, @Res() response): string { + if (!quero.view) { + return response.status(302).redirect('/error?code=404'); + } + + const file = base64url.decode(quero.view); + + const regexp = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/; + if (regexp.test(file)) { + return response.render('iframe', { name: 'Market Label', url: file }); + } + + if (!fs.existsSync(file)) { + return response.status(302).redirect('/error?code=404'); + } + + const mime = lookup(file); + const uri = file.replace(config.getPublicPath(), ''); + const name = path.basename(uri); + if (['application/pdf', 'text/html'].includes(mime)) { + return response.render('iframe', { name, url: `${config.getAssetURI()}${uri}` }); + } + + return response + .headers({ + 'Content-Type': mime, + 'Content-Disposition': `attachment; filename=${name}`, + }) + .send(fs.readFileSync(file)); + } } diff --git a/src/app.module.ts b/src/app.module.ts index 8bf9e16..6ec292e 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -10,6 +10,7 @@ import { routes } from './app.routes'; import { CustomerModule } from './app/customer/customer.module'; import { OwnerModule } from './app/owner/owner.module'; import { RestaurantModule } from './app/restaurant/restaurant.module'; +import { StaffModule } from './app/staff/staff.module'; @Module({ imports: [ @@ -20,6 +21,7 @@ import { RestaurantModule } from './app/restaurant/restaurant.module'; OwnerModule, RestaurantModule, CustomerModule, + StaffModule, ], controllers: [AppController], providers: [SocketioGateway], diff --git a/src/app.routes.ts b/src/app.routes.ts index 000b6b9..99d2bd2 100644 --- a/src/app.routes.ts +++ b/src/app.routes.ts @@ -1,12 +1,15 @@ -import { Routes } from 'nest-router'; +import type { Routes } from 'nest-router'; import { CustomerAuthModule } from './app/customer/auth/auth.module'; import { CustomerModule } from './app/customer/customer.module'; import { CustomerOrderModule } from './app/customer/order/order.module'; +import { CustomerTableModule } from './app/customer/table/table.module'; import { OwnerAuthModule } from './app/owner/auth/auth.module'; import { OwnerModule } from './app/owner/owner.module'; import { OwnerProfileModule } from './app/owner/profile/profile.module'; import { OwnerCategoryModule } from './app/owner/restaurant/category/category.module'; import { OwnerLocationModule } from './app/owner/restaurant/location/location.module'; +import { OwnerNotificationModule } from './app/owner/restaurant/notification/notification.module'; +import { OwnerOrderModule } from './app/owner/restaurant/order/order.module'; import { OwnerProductModule } from './app/owner/restaurant/product/product.module'; import { OwnerRestaurantModule } from './app/owner/restaurant/restaurant.module'; import { OwnerStaffModule } from './app/owner/restaurant/staff/staff.module'; @@ -14,6 +17,17 @@ import { OwnerStockModule } from './app/owner/restaurant/stock/stock.module'; import { OwnerTableModule } from './app/owner/restaurant/table/table.module'; import { OwnerVariantModule } from './app/owner/restaurant/variant/variant.module'; import { RestaurantModule } from './app/restaurant/restaurant.module'; +import { StaffAuthModule } from './app/staff/auth/auth.module'; +import { StaffProfileModule } from './app/staff/profile/profile.module'; +import { StaffCategoryModule } from './app/staff/restaurant/category/category.module'; +import { StafffNotificationModule } from './app/staff/restaurant/notification/notification.module'; +import { StaffOrderModule } from './app/staff/restaurant/order/order.module'; +import { StaffProductModule } from './app/staff/restaurant/product/product.module'; +import { StaffRestaurantModule } from './app/staff/restaurant/restaurant.module'; +import { StaffStockModule } from './app/staff/restaurant/stock/stock.module'; +import { StaffTableModule } from './app/staff/restaurant/table/table.module'; +import { StaffVariantModule } from './app/staff/restaurant/variant/variant.module'; +import { StaffModule } from './app/staff/staff.module'; export const routes: Routes = [ // { path: '/auth', module: AuthModule }, @@ -56,10 +70,62 @@ export const routes: Routes = [ path: '/:restaurant_id/stocks', module: OwnerStockModule, }, + { + path: '/:restaurant_id/orders', + module: OwnerOrderModule, + }, + { + path: '/:restaurant_id/notifications', + module: OwnerNotificationModule, + }, + ], + }, + ], + }, + + { + path: '/staff', + module: StaffModule, + children: [ + { path: '/auth', module: StaffAuthModule }, + { path: '/me', module: StaffProfileModule }, + { + path: '/restaurants', + module: StaffRestaurantModule, + children: [ + { + path: '/:restaurant_id/orders', + module: StaffOrderModule, + }, + { + path: '/:restaurant_id/stocks', + module: StaffStockModule, + }, + { + path: '/:restaurant_id/tables', + module: StaffTableModule, + }, + { + path: '/:restaurant_id/categories', + module: StaffCategoryModule, + }, + { + path: '/:restaurant_id/variants', + module: StaffVariantModule, + }, + { + path: '/:restaurant_id/products', + module: StaffProductModule, + }, + { + path: '/:restaurant_id/notifications', + module: StafffNotificationModule, + }, ], }, ], }, + { path: '/restaurants', module: RestaurantModule, @@ -76,6 +142,10 @@ export const routes: Routes = [ path: '/orders', module: CustomerOrderModule, }, + { + path: '/tables', + module: CustomerTableModule, + }, ], }, ]; diff --git a/src/app/customer/customer.module.ts b/src/app/customer/customer.module.ts index fbed1cc..ed788ce 100644 --- a/src/app/customer/customer.module.ts +++ b/src/app/customer/customer.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { CustomerAuthModule } from './auth/auth.module'; import { CustomerController } from './customer.controller'; import { CustomerOrderModule } from './order/order.module'; +import { CustomerTableModule } from './table/table.module'; @Module({ - imports: [CustomerAuthModule, CustomerOrderModule], + imports: [CustomerAuthModule, CustomerOrderModule, CustomerTableModule], controllers: [CustomerController], providers: [], }) diff --git a/src/app/customer/order/detail.controller.ts b/src/app/customer/order/detail.controller.ts index 673b7d4..62c3586 100644 --- a/src/app/customer/order/detail.controller.ts +++ b/src/app/customer/order/detail.controller.ts @@ -3,7 +3,7 @@ import { AuthGuard } from '@core/guards/auth.guard'; import { Customer } from '@db/entities/core/customer.entity'; import { Order, OrderStatus } from '@db/entities/core/order.entity'; import { Table, TableStatus } from '@db/entities/owner/table.entity'; -import { OrderTransformer } from '@db/transformers/order.tranformer'; +import { OrderTransformer } from '@db/transformers/order.transformer'; import { ValidationException } from '@lib/exceptions/validation.exception'; import { Validator } from '@lib/helpers/validator.helper'; import AppDataSource from '@lib/typeorm/datasource.typeorm'; diff --git a/src/app/customer/order/order.controller.ts b/src/app/customer/order/order.controller.ts index 8d83681..1a6e2ef 100644 --- a/src/app/customer/order/order.controller.ts +++ b/src/app/customer/order/order.controller.ts @@ -2,6 +2,7 @@ import { Me } from '@core/decorators/user.decorator'; import { AuthGuard } from '@core/guards/auth.guard'; import { CustomerService } from '@core/services/customer.service'; import { Customer, CustomerStatus } from '@db/entities/core/customer.entity'; +import { Notification, NotificationType } from '@db/entities/core/notification.entity'; import { OrderProduct, OrderProductStatus } from '@db/entities/core/order-product.entity'; import { Order, OrderStatus } from '@db/entities/core/order.entity'; import { ProductStock } from '@db/entities/owner/product-stock.entity'; @@ -10,164 +11,199 @@ import { Product } from '@db/entities/owner/product.entity'; import { Table, TableStatus } from '@db/entities/owner/table.entity'; import { Variant, VariantStatus } from '@db/entities/owner/variant.entity'; import { IOrderDetail } from '@db/interfaces/order.interface'; -import { OrderTransformer } from '@db/transformers/order.tranformer'; +import { OrderTransformer } from '@db/transformers/order.transformer'; import { ValidationException } from '@lib/exceptions/validation.exception'; import { sequenceNumber } from '@lib/helpers/utils.helper'; import { Validator } from '@lib/helpers/validator.helper'; +import Socket from '@lib/pubsub/pubsub.lib'; import AppDataSource from '@lib/typeorm/datasource.typeorm'; +import { uuid } from '@lib/uid/uuid.library'; import { BadRequestException, Body, Controller, Get, Post, Res, UseGuards } from '@nestjs/common'; import { get } from 'lodash'; +import { IsNull } from 'typeorm'; @Controller() export class OrderController { constructor(private custService: CustomerService) {} static async createOrder(newOrder: IOrderDetail, customer: Customer): Promise { - const order = new Order(); - await AppDataSource.transaction(async (manager) => { - const orderProducts = newOrder.products || []; - if (!orderProducts.length) { - throw new BadRequestException(`Order has no product items`); - } - - if (orderProducts.some((row) => row.qty < 1)) { - throw new BadRequestException(`Order has invalid product item qty of 0`); - } - - const table = await manager.getRepository(Table).findOneBy({ id: order.table_id }); + try { + const order = new Order(); + await AppDataSource.transaction(async (manager) => { + const table = await manager.getRepository(Table).findOneBy({ id: order.table_id, status: TableStatus.Available }); - if (table.status !== TableStatus.Available) { - throw new BadRequestException('Sorry, this table is not available right now'); - } - - await manager.getRepository(Table).update(order.table_id, { status: TableStatus.InUse }); - - order.restaurant_id = newOrder.restaurant_id; - order.location_id = newOrder.location_id; - order.table_id = newOrder.table_id; - order.status = newOrder.status; - order.note = newOrder.note; - order.customer_id = customer?.id || null; - order.number = ''; // Will be update it later - - await manager.getRepository(Order).save(order); - - for (const product of newOrder.products) { - const variant = await manager.getRepository(ProductVariant).findOneOrFail({ - where: { - id: product.id, - restaurant_id: newOrder.restaurant_id, - status: VariantStatus.Available, - }, - }); + if (!table) { + throw new BadRequestException('Sorry, this table is not available right now'); + } - const stock = await manager.getRepository(ProductStock).findOneOrFail({ - where: { - restaurant_id: newOrder.restaurant_id, - location_id: newOrder.location_id, - variant_id: variant.id, - }, - }); + const orderProducts = newOrder.products || []; + if (!orderProducts.length) { + throw new BadRequestException(`Order has no product items`); + } - if (product.qty < stock.available) { - const product = await manager.getRepository(Product).findOneByOrFail({ id: variant.product_id }); + if (orderProducts.some((row) => row.qty < 1)) { + throw new BadRequestException(`Order has invalid product item qty of 0`); + } - let productName = product.name; + order.restaurant_id = newOrder.restaurant_id; + order.location_id = table.location_id; + order.table_id = newOrder.table_id; + order.status = newOrder.status; + order.note = newOrder.note; + order.customer_id = customer?.id || null; + order.discount = null; + order.customer_name = newOrder.customer_name; + order.customer_phone = newOrder.customer_phone; + order.number = ''; // Will be update it later + + await manager.getRepository(Order).save(order); + + const orderedProducts: OrderProduct[] = []; + for (const product of newOrder.products) { + const variant = await manager.getRepository(ProductVariant).findOneOrFail({ + where: { + product_id: product.id, + variant_id: get(product, 'variant_id', null) || IsNull(), + restaurant_id: newOrder.restaurant_id, + status: VariantStatus.Available, + }, + }); + + const stock = await manager.getRepository(ProductStock).findOneOrFail({ + where: { + restaurant_id: order.restaurant_id, + location_id: order.location_id, + variant_id: variant.id, + }, + }); + + if (product.qty > stock.available) { + const product = await manager.getRepository(Product).findOneByOrFail({ id: variant.product_id }); + + let productName = product.name; + + if (variant.variant_id) { + const vari = await manager.getRepository(Variant).findOneByOrFail({ id: variant.variant_id }); + + productName += ` - ${vari.name}`; + } + + throw new BadRequestException(`Sorry, ${productName} stock is insufficient`); + } - if (variant.variant_id) { - const vari = await manager.getRepository(Variant).findOneByOrFail({ id: variant.variant_id }); + let orderProduct = new OrderProduct(); - productName += ` - ${vari.name}`; + const checkOrderProduct = await manager.getRepository(OrderProduct).findOneBy({ + order_id: order.id, + product_variant_id: variant.id, + }); + if (checkOrderProduct) { + orderProduct = checkOrderProduct; } - throw new BadRequestException(`Sorry, ${productName} stock is insufficient`); - } - - let orderProduct = new OrderProduct(); + orderProduct.order_id = order.id; + orderProduct.product_variant_id = variant.id; + orderProduct.qty = product.qty; + orderProduct.price = variant.price * product.qty; + orderProduct.status = OrderProductStatus.WaitingApproval; + await manager.getRepository(OrderProduct).save(orderProduct); - const checkOrderProduct = await manager.getRepository(OrderProduct).findOneBy({ - order_id: order.id, - product_variant_id: variant.id, - }); - if (checkOrderProduct) { - orderProduct = checkOrderProduct; + orderedProducts.push(orderProduct); } - orderProduct.order_id = order.id; - orderProduct.product_variant_id = variant.id; - orderProduct.qty = product.qty; - orderProduct.price = variant.price; - orderProduct.status = OrderProductStatus.WaitingApproval; - await manager.getRepository(OrderProduct).save(orderProduct); - } + // Updare Order Number + await manager.getRepository(Order).update(order.id, { + number: sequenceNumber(order.uid), + gross_total: orderedProducts.reduce((price, a) => price + a.price, 0), + }); - // Updare Order Number - await manager.getRepository(Order).update(order.id, { - number: sequenceNumber(order.uid), + table.status = TableStatus.InUse; + await manager.getRepository(Table).save(table); }); - }); - await order.reload(); + await order.reload(); - return order; + return order; + } catch (error) { + throw error; + } } @Post() async createOrder(@Res() response, @Body() body) { const rules = { restaurant_id: 'required|uid', - location_id: 'required|uid', table_id: 'required|uid', customer_name: 'required|safe_text', customer_phone: 'phone', products: 'required|array', note: 'safe_text', }; - const validation = Validator.init(body, rules); - if (validation.fails()) { - throw new ValidationException(validation); - } - - const newOrder: IOrderDetail = { - restaurant_id: get(body, 'restaurant_id', null), - location_id: get(body, 'location_id', null), - table_id: get(body, 'table_id', null), - status: OrderStatus.WaitingApproval, - customer_name: get(body, 'customer_name', 'Guest'), - customer_phone: get(body, 'customer_phone', null), - note: get(body, 'note', null), - gross_total: 0, - discount: 0, - fee: 0, - net_total: 0, - products: get(body, 'products', []), - }; - // @TODO: Manage new or existing customer - let customer: Customer = null; - if (body.customer_phone) { - customer = await Customer.findOneBy({ phone: body.customer_phone }); - - if (!customer) { - // @TODO: Create new customer and also verify it! - customer.name = newOrder.customer_name; - customer.phone = newOrder.customer_phone; - const newCustomer = await this.custService.register(customer); - const token = await this.custService.login(newCustomer); - return response.data(token); + try { + const validation = Validator.init(body, rules); + if (validation.fails()) { + throw new ValidationException(validation); } - if (customer.status === CustomerStatus.Verify) { - const token = await this.custService.login(customer); - return response.data(token); + const newOrder: IOrderDetail = { + restaurant_id: get(body, 'restaurant_id', null), + table_id: get(body, 'table_id', null), + status: OrderStatus.WaitingApproval, + customer_name: get(body, 'customer_name', 'Guest'), + customer_phone: get(body, 'customer_phone', null), + note: get(body, 'note', null), + gross_total: 0, + discount: 0, + fee: 0, + net_total: 0, + products: get(body, 'products', []), + }; + + // @TODO: Manage new or existing customer + let customer: Customer = null; + if (body.customer_phone) { + customer = await Customer.findOneBy({ phone: body.customer_phone }); + + if (!customer) { + // @TODO: Create new customer and also verify it! + customer.name = newOrder.customer_name; + customer.phone = newOrder.customer_phone; + const newCustomer = await this.custService.register(customer); + const token = await this.custService.login(newCustomer); + return response.data(token); + } + + if (customer.status === CustomerStatus.Verify) { + const token = await this.custService.login(customer); + return response.data(token); + } } - } - const order = await OrderController.createOrder(newOrder, customer); + const order = await OrderController.createOrder(newOrder, customer); - // @TODO: Socket to Cashier + const notification = new Notification(); + notification.title = 'New Order'; + notification.content = `Order ${order.number} has been created`; + notification.actor = customer ? customer.name : newOrder.customer_name; + notification.location_id = order.location_id; + notification.restaurant_id = order.restaurant_id; + notification.type = NotificationType.OrderCreated; + notification.order_id = order.id; + + await AppDataSource.transaction(async (manager) => { + await manager.getRepository(Notification).save(notification); + }); - return response.item(order, OrderTransformer); + Socket.getInstance().notify(notification.location_id, { + request_id: uuid(), + data: notification, + }); + + return response.item(order, OrderTransformer); + } catch (error) { + throw error; + } } @Get() diff --git a/src/app/customer/table/table.controller.ts b/src/app/customer/table/table.controller.ts new file mode 100644 index 0000000..b9af10f --- /dev/null +++ b/src/app/customer/table/table.controller.ts @@ -0,0 +1,28 @@ +import { Restaurant, RestaurantStatus } from '@db/entities/owner/restaurant.entity'; +import { Table } from '@db/entities/owner/table.entity'; +import { TableTransformer } from '@db/transformers/table.transformer'; + +import { GenericException } from '@lib/exceptions/generic.exception'; +import { Controller, Get, NotFoundException, Param, Res } from '@nestjs/common'; +import { get } from 'lodash'; + +@Controller() +export class TableController { + @Get('/:id') + async checkTable(@Param() params, @Res() response) { + const id = get(params, 'id', null); + const table = await Table.findOneBy({ id }); + + if (!table) { + throw new NotFoundException('Table not found!'); + } + + const restaurant = await Restaurant.findOneBy({ id: table.restaurant_id }); + + if (restaurant.status !== RestaurantStatus.Active) { + throw new GenericException(`Sorry, restaurant is inactive`); + } + + return response.item(table, TableTransformer); + } +} diff --git a/src/app/customer/table/table.module.ts b/src/app/customer/table/table.module.ts new file mode 100644 index 0000000..dd4e77c --- /dev/null +++ b/src/app/customer/table/table.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { TableController } from './table.controller'; + +@Module({ + imports: [], + controllers: [TableController], + providers: [], +}) +export class CustomerTableModule {} diff --git a/src/app/owner/restaurant/notification/notification.controller.ts b/src/app/owner/restaurant/notification/notification.controller.ts new file mode 100644 index 0000000..e23d0d2 --- /dev/null +++ b/src/app/owner/restaurant/notification/notification.controller.ts @@ -0,0 +1,49 @@ +import { Loc } from '@core/decorators/location.decorator'; +import { Rest } from '@core/decorators/restaurant.decorator'; +import { OwnerAuthGuard } from '@core/guards/auth.guard'; +import { OwnerGuard } from '@core/guards/owner.guard'; +import { PermAct, PermOwner } from '@core/services/role.service'; +import { Notification } from '@db/entities/core/notification.entity'; +import { Location } from '@db/entities/owner/location.entity'; +import { Restaurant } from '@db/entities/owner/restaurant.entity'; +import { NotificationTransformer } from '@db/transformers/notification.transformer'; +import { ValidationException } from '@lib/exceptions/validation.exception'; +import { Validator } from '@lib/helpers/validator.helper'; +import { Permissions } from '@lib/rbac'; +import AppDataSource from '@lib/typeorm/datasource.typeorm'; +import { Body, Controller, Get, Put, Res, UseGuards } from '@nestjs/common'; +import { In } from 'typeorm'; + +@Controller() +@UseGuards(OwnerAuthGuard()) +export class NotificationController { + @Get() + @UseGuards(OwnerGuard) + @Permissions(`${PermOwner.Notification}@${PermAct.R}`) + async index(@Rest() rest: Restaurant, @Loc() loc: Location, @Res() response) { + const query = AppDataSource.createQueryBuilder(Notification, 't1').where({ restaurant_id: rest.id }); + + if (loc) { + query.andWhere('t1.location_id = :locId', { locId: loc.id }); + } + + const results = await query.search().sort().getPaged(); + await response.paginate(results, NotificationTransformer); + } + + @Put() + @UseGuards(OwnerGuard) + @Permissions(`${PermOwner.Notification}@${PermAct.U}`) + async mark(@Body() body, @Res() response) { + const rules = { + ids: `required|array|uid`, + }; + const validation = Validator.init(body, rules); + if (validation.fails()) { + throw new ValidationException(validation); + } + + await Notification.update({ id: In(body.ids) }, { is_read: true }); + return response.noContent(); + } +} diff --git a/src/app/owner/restaurant/notification/notification.module.ts b/src/app/owner/restaurant/notification/notification.module.ts new file mode 100644 index 0000000..ec3e8ee --- /dev/null +++ b/src/app/owner/restaurant/notification/notification.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { NotificationController } from './notification.controller'; + +@Module({ + imports: [], + controllers: [NotificationController], + providers: [], +}) +export class OwnerNotificationModule {} diff --git a/src/app/owner/restaurant/order/detail.controller.ts b/src/app/owner/restaurant/order/detail.controller.ts new file mode 100644 index 0000000..79b4a41 --- /dev/null +++ b/src/app/owner/restaurant/order/detail.controller.ts @@ -0,0 +1,172 @@ +import { Rest } from '@core/decorators/restaurant.decorator'; +import { Me } from '@core/decorators/user.decorator'; +import { OwnerAuthGuard } from '@core/guards/auth.guard'; +import { OwnerGuard } from '@core/guards/owner.guard'; +import { PermAct, PermOwner } from '@core/services/role.service'; +import { Notification, NotificationType } from '@db/entities/core/notification.entity'; +import { OrderProduct, OrderProductStatus } from '@db/entities/core/order-product.entity'; +import { Order, OrderStatus } from '@db/entities/core/order.entity'; +import { Owner } from '@db/entities/owner/owner.entity'; +import { ProductStock } from '@db/entities/owner/product-stock.entity'; +import { Table, TableStatus } from '@db/entities/owner/table.entity'; +import { OrderTransformer } from '@db/transformers/order.transformer'; +import { GenericException } from '@lib/exceptions/generic.exception'; +import { ValidationException } from '@lib/exceptions/validation.exception'; +import { time } from '@lib/helpers/time.helper'; +import { Validator } from '@lib/helpers/validator.helper'; +import Socket from '@lib/pubsub/pubsub.lib'; +import { Permissions } from '@lib/rbac'; +import AppDataSource from '@lib/typeorm/datasource.typeorm'; +import { uuid } from '@lib/uid/uuid.library'; +import { Body, Controller, Get, Param, Put, Res, UseGuards } from '@nestjs/common'; +import { In } from 'typeorm'; + +@Controller(':order_id') +@UseGuards(OwnerAuthGuard()) +export class DetailController { + static async action(order: Order, action: OrderStatus, actor: Owner) { + try { + switch (order.status) { + case OrderStatus.WaitingApproval: { + // @TODO: How "REJECTED" flow works? + if (![OrderStatus.Confirmed, OrderStatus.Cancelled].includes(action)) { + throw new GenericException(`Order ${order.number} must be confirmed first.`); + } + + // @TODO: Able to cancel/decline Product and Recalculate Gross Total + + order.status = action; + break; + } + case OrderStatus.Confirmed: { + if (![OrderStatus.Preparing].includes(action)) { + throw new GenericException(`Order ${order.number} can't be ${action}.`); + } + + // @TODO: Able to cancel/decline Product and Recalculate Gross Total + + order.status = OrderStatus.Preparing; + break; + } + case OrderStatus.Preparing: { + if (![OrderStatus.Served].includes(action)) { + throw new GenericException(`Order ${order.number} can't be ${action}.`); + } + + order.status = OrderStatus.Served; + break; + } + case OrderStatus.Served: { + if (![OrderStatus.WaitingPayment].includes(action)) { + throw new GenericException(`Order ${order.number} can't be ${action}.`); + } + + order.status = OrderStatus.WaitingPayment; + break; + } + case OrderStatus.WaitingPayment: { + if (![OrderStatus.Completed].includes(action)) { + throw new GenericException(`Order ${order.number} can't be ${action}.`); + } + + order.status = OrderStatus.Completed; + order.billed_at = time().toDate(); + break; + } + case OrderStatus.Completed: + // @TODO: Decrease stock + const orderProducts = await OrderProduct.findBy({ order_id: order.id }); + const productStocks = await ProductStock.findBy({ + variant_id: In(orderProducts.map((val) => val.product_variant_id)), + }); + + for (const stock of productStocks) { + const orderProduct = orderProducts.find((val) => val.product_variant_id === stock.variant_id); + if (orderProduct) { + stock.onhand -= orderProduct.qty; + stock.allocated -= orderProduct.qty; + stock.actor = actor ? actor.logName : 'System'; + stock.last_action = `Order ${order.number}`; + await AppDataSource.transaction(async (manager) => { + await manager.getRepository(ProductStock).save(stock); + }); + } + } + + break; + case OrderStatus.Cancelled: { + if (![OrderStatus.WaitingApproval].includes(action)) { + throw new GenericException(`Order ${order.number} can't be ${action}.`); + } + + order.status = OrderStatus.Cancelled; + break; + } + } + + const notification = new Notification(); + notification.title = 'Order Updated'; + notification.content = JSON.stringify(order); + notification.actor = 'System'; + notification.location_id = order.location_id; + notification.restaurant_id = order.restaurant_id; + notification.type = NotificationType.OrderUpdate; + notification.order_id = order.id; + + await AppDataSource.transaction(async (manager) => { + await manager.getRepository(Order).save(order); + + if ([OrderStatus.Cancelled].includes(order.status)) { + await manager.getRepository(OrderProduct).update({ order_id: order.id }, { status: OrderProductStatus.Cancelled }); + } + + if ([OrderStatus.Completed, OrderStatus.Cancelled].includes(order.status)) { + const table = await manager.getRepository(Table).findOneBy({ id: order.table_id }); + if (!table) { + throw new Error(`Table not found with ID: ${order.table_id}`); + } + table.status = TableStatus.Available; + await manager.getRepository(Table).save(table); + } + + await manager.getRepository(Notification).save(notification); + }); + + Socket.getInstance().notify(notification.order_id, { + request_id: uuid(), + data: notification, + }); + } catch (error) { + throw error; + } + } + + @Get() + @UseGuards(OwnerGuard) + @Permissions(`${PermOwner.Order}@${PermAct.R}`) + async show(@Rest() rest, @Res() response, @Param() param) { + const order = await Order.findOneByOrFail({ restaurant_id: rest.id, id: param.order_id }); + await response.item(order, OrderTransformer); + } + + @Put() + @UseGuards(OwnerGuard) + @Permissions(`${PermOwner.Order}@${PermAct.U}`) + async action(@Body() body, @Param() param, @Res() response, @Me() me: Owner) { + const rules = { + action: `required|in:${Object.values(OrderStatus) + .filter((val) => val !== 'waiting_approval') + .join(',')}`, + }; + const validation = Validator.init(body, rules); + if (validation.fails()) { + throw new ValidationException(validation); + } + + const order = await Order.findOneByOrFail({ id: param.order_id }); + await DetailController.action(order, body.action, me); + await order.reload(); + + return response.item(order, OrderTransformer); + } +} diff --git a/src/app/owner/restaurant/order/order.controller.ts b/src/app/owner/restaurant/order/order.controller.ts new file mode 100644 index 0000000..471508a --- /dev/null +++ b/src/app/owner/restaurant/order/order.controller.ts @@ -0,0 +1,35 @@ +import { Loc } from '@core/decorators/location.decorator'; +import { Quero } from '@core/decorators/quero.decorator'; +import { Rest } from '@core/decorators/restaurant.decorator'; +import { OwnerAuthGuard } from '@core/guards/auth.guard'; +import { OwnerGuard } from '@core/guards/owner.guard'; +import { PermAct, PermOwner } from '@core/services/role.service'; +import { Order } from '@db/entities/core/order.entity'; +import { Location } from '@db/entities/owner/location.entity'; +import { Restaurant } from '@db/entities/owner/restaurant.entity'; +import { OrderTransformer } from '@db/transformers/order.transformer'; +import { Permissions } from '@lib/rbac'; +import AppDataSource from '@lib/typeorm/datasource.typeorm'; +import { Controller, Get, Res, UseGuards } from '@nestjs/common'; + +@Controller() +@UseGuards(OwnerAuthGuard()) +export class OrderController { + @Get() + @UseGuards(OwnerGuard) + @Permissions(`${PermOwner.Order}@${PermAct.R}`) + async index(@Rest() rest: Restaurant, @Loc() loc: Location, @Res() response, @Quero() quero) { + const query = AppDataSource.createQueryBuilder(Order, 't1').where({ restaurant_id: rest.id }); + + if (loc) { + query.andWhere('t1.location_id = :locId', { locId: loc.id }); + } + + if (quero.status) { + query.andWhere('t1.status = :status', { status: quero.status }); + } + + const results = await query.search().sort().getPaged(); + await response.paginate(results, OrderTransformer); + } +} diff --git a/src/app/owner/restaurant/order/order.module.ts b/src/app/owner/restaurant/order/order.module.ts new file mode 100644 index 0000000..f91c7fd --- /dev/null +++ b/src/app/owner/restaurant/order/order.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { DetailController } from './detail.controller'; +import { OrderController } from './order.controller'; + +@Module({ + imports: [], + controllers: [OrderController, DetailController], + providers: [], +}) +export class OwnerOrderModule {} diff --git a/src/app/owner/restaurant/product/category.controller.ts b/src/app/owner/restaurant/product/category.controller.ts new file mode 100644 index 0000000..99a6340 --- /dev/null +++ b/src/app/owner/restaurant/product/category.controller.ts @@ -0,0 +1,51 @@ +import { Rest } from '@core/decorators/restaurant.decorator'; +import { OwnerAuthGuard } from '@core/guards/auth.guard'; +import { OwnerGuard } from '@core/guards/owner.guard'; +import { ProductService } from '@core/services/product.service'; +import { PermAct, PermOwner } from '@core/services/role.service'; +import { ProductCategory } from '@db/entities/owner/product-category.entity'; +import { Product } from '@db/entities/owner/product.entity'; +import { ProductTransformer } from '@db/transformers/product.transformer'; +import { ValidationException } from '@lib/exceptions/validation.exception'; +import { Validator } from '@lib/helpers/validator.helper'; +import { Permissions } from '@lib/rbac'; +import { BadRequestException, Body, Controller, Delete, Param, Post, Res, UseGuards } from '@nestjs/common'; + +@Controller(':product_id') +@UseGuards(OwnerAuthGuard()) +export class CategoryController { + constructor(private productService: ProductService) {} + + @Post('/categories') + @UseGuards(OwnerGuard) + @Permissions(`${PermOwner.Product}@${PermAct.U}`) + async addCategory(@Body() body, @Res() response, @Param() param, @Rest() rest) { + const rules = { + category_ids: 'required|array|uid', + }; + const validation = Validator.init(body, rules); + if (validation.fails()) { + throw new ValidationException(validation); + } + + const product = await this.productService.assignCategories(rest, param.product_id, body.category_ids); + + await response.item(product, ProductTransformer); + } + + @Delete('/categories/:category_id') + @UseGuards(OwnerGuard) + @Permissions(`${PermOwner.Product}@${PermAct.D}`) + async deleteCategory(@Param() param, @Res() response, @Rest() rest) { + if (!param.category_id) { + throw new BadRequestException(); + } + + const product = await Product.findOrFail({ where: { id: param.product_id, restaurant_id: rest.id } }); + const category = await ProductCategory.findOneByOrFail({ id: param.category_id, product_id: product.id }); + + await category.remove(); + + return response.noContent(); + } +} diff --git a/src/app/owner/restaurant/product/detail.controller.ts b/src/app/owner/restaurant/product/detail.controller.ts index fd43d72..a5c5d6d 100644 --- a/src/app/owner/restaurant/product/detail.controller.ts +++ b/src/app/owner/restaurant/product/detail.controller.ts @@ -5,8 +5,6 @@ import { OwnerGuard } from '@core/guards/owner.guard'; import { AwsService } from '@core/services/aws.service'; import { PermAct, PermOwner } from '@core/services/role.service'; import { Media } from '@db/entities/core/media.entity'; -import { Category } from '@db/entities/owner/category.entity'; -import { ProductCategory } from '@db/entities/owner/product-category.entity'; import { ProductStock } from '@db/entities/owner/product-stock.entity'; import { Product } from '@db/entities/owner/product.entity'; import { ProductTransformer } from '@db/transformers/product.transformer'; @@ -15,7 +13,7 @@ import { ValidationException } from '@lib/exceptions/validation.exception'; import { Validator } from '@lib/helpers/validator.helper'; import { Permissions } from '@lib/rbac'; import { BadRequestException, Body, Controller, Delete, Get, Param, Post, Put, Req, Res, UseGuards } from '@nestjs/common'; -import { In, Not } from 'typeorm'; +import { Not } from 'typeorm'; @Controller(':product_id') @UseGuards(OwnerAuthGuard()) @@ -96,57 +94,6 @@ export class DetailController { return response.noContent(); } - @Post('/categories') - @UseGuards(OwnerGuard) - @Permissions(`${PermOwner.Product}@${PermAct.U}`) - async addCategory(@Body() body, @Res() response, @Param() param, @Rest() rest) { - const rules = { - category_ids: 'required|array|uid', - }; - const validation = Validator.init(body, rules); - if (validation.fails()) { - throw new ValidationException(validation); - } - - const product = await Product.findOrFail({ where: { id: param.product_id, restaurant_id: rest.id } }); - const prodCategories: ProductCategory[] = []; - - const categories = await Category.findBy({ id: In(body.category_ids) }); - for (const category of categories) { - const isExist = await ProductCategory.exists({ where: { product_id: product.id, category_id: category.id } }); - if (isExist) { - continue; - } - - const pcat = new ProductCategory(); - pcat.product_id = product.id; - pcat.category_id = category.id; - prodCategories.push(pcat); - } - - if (prodCategories.length > 0) { - await ProductCategory.save(prodCategories); - } - - await response.item(product, ProductTransformer); - } - - @Delete('/categories/:category_id') - @UseGuards(OwnerGuard) - @Permissions(`${PermOwner.Product}@${PermAct.D}`) - async deleteCategory(@Param() param, @Res() response, @Rest() rest) { - if (!param.category_id) { - throw new BadRequestException(); - } - - const product = await Product.findOrFail({ where: { id: param.product_id, restaurant_id: rest.id } }); - const category = await ProductCategory.findOneByOrFail({ id: param.category_id, product_id: product.id }); - - await category.remove(); - - return response.noContent(); - } - @Get('/stocks') @UseGuards(OwnerGuard) @Permissions(`${PermOwner.Product}@${PermAct.R}`) diff --git a/src/app/owner/restaurant/product/product.controller.ts b/src/app/owner/restaurant/product/product.controller.ts index 0f9250b..a0e8c30 100644 --- a/src/app/owner/restaurant/product/product.controller.ts +++ b/src/app/owner/restaurant/product/product.controller.ts @@ -1,23 +1,21 @@ import { Rest } from '@core/decorators/restaurant.decorator'; import { OwnerAuthGuard } from '@core/guards/auth.guard'; import { OwnerGuard } from '@core/guards/owner.guard'; +import { ProductService } from '@core/services/product.service'; import { PermAct, PermOwner } from '@core/services/role.service'; -import { Category } from '@db/entities/owner/category.entity'; -import { ProductCategory } from '@db/entities/owner/product-category.entity'; -import { ProductVariant } from '@db/entities/owner/product-variant.entity'; import { Product, ProductStatus } from '@db/entities/owner/product.entity'; -import { Variant, VariantStatus } from '@db/entities/owner/variant.entity'; import { ProductTransformer } from '@db/transformers/product.transformer'; import { ValidationException } from '@lib/exceptions/validation.exception'; import { Validator } from '@lib/helpers/validator.helper'; import { Permissions } from '@lib/rbac'; import AppDataSource from '@lib/typeorm/datasource.typeorm'; -import { BadRequestException, Body, Controller, Get, Post, Res, UseGuards } from '@nestjs/common'; -import { In } from 'typeorm'; +import { Body, Controller, Get, Post, Res, UseGuards } from '@nestjs/common'; @Controller() @UseGuards(OwnerAuthGuard()) export class ProductController { + constructor(private productService: ProductService) {} + @Get() @UseGuards(OwnerGuard) @Permissions(`${PermOwner.Product}@${PermAct.R}`) @@ -48,72 +46,7 @@ export class ProductController { throw new ValidationException(validation); } - const productExist = await Product.exists({ where: { sku: body.sku, restaurant_id: rest.id } }); - if (productExist) { - throw new BadRequestException('Product SKU has already existed.'); - } - - const prod = new Product(); - prod.sku = body.sku; - prod.name = body.name; - prod.description = body.description; - prod.price = body.price; - prod.status = body.status; - prod.restaurant_id = rest.id; - - let categories: Category[] = []; - let variants: Variant[] = []; - - if (body.category_ids.length > 0) { - categories = await Category.find({ where: { id: In(body.category_ids), restaurant_id: rest.id } }); - } - - if (body.variant_ids.length > 0) { - variants = await Variant.find({ where: { id: In(body.variant_ids), restaurant_id: rest.id } }); - } - - await AppDataSource.transaction(async (manager) => { - // Save Product - await manager.getRepository(Product).save(prod); - - // Save Category - if (categories.length > 0) { - const pcats: ProductCategory[] = []; - for (const category of categories) { - const pcat = new ProductCategory(); - pcat.product_id = prod.id; - pcat.category_id = category.id; - pcats.push(pcat); - } - - await manager.getRepository(ProductCategory).save(pcats); - } - - const pvars: ProductVariant[] = []; - - // Create default variant for single product - const pvar = new ProductVariant(); - pvar.product_id = prod.id; - pvar.price = prod.price; - pvar.status = VariantStatus.Available; - pvar.restaurant_id = rest.id; - pvars.push(pvar); - - // Save Variants - if (variants.length > 0) { - for (const variant of variants) { - const vari = new ProductVariant(); - vari.status = variant.status; - vari.product_id = prod.id; - vari.variant_id = variant.id; - vari.price = variant.price; - vari.restaurant_id = rest.id; - pvars.push(vari); - } - } - - await manager.getRepository(ProductVariant).save(pvars); - }); + const prod = await this.productService.create(rest, body); return response.item(prod, ProductTransformer); } diff --git a/src/app/owner/restaurant/product/product.module.ts b/src/app/owner/restaurant/product/product.module.ts index 29be23a..3ab3a5f 100644 --- a/src/app/owner/restaurant/product/product.module.ts +++ b/src/app/owner/restaurant/product/product.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common'; +import { CategoryController } from './category.controller'; import { DetailController } from './detail.controller'; import { ProductController } from './product.controller'; import { VariantController } from './variant.controller'; @Module({ imports: [], - controllers: [ProductController, DetailController, VariantController], + controllers: [ProductController, DetailController, CategoryController, VariantController], providers: [], }) export class OwnerProductModule {} diff --git a/src/app/owner/restaurant/product/variant.controller.ts b/src/app/owner/restaurant/product/variant.controller.ts index b53c313..911fec6 100644 --- a/src/app/owner/restaurant/product/variant.controller.ts +++ b/src/app/owner/restaurant/product/variant.controller.ts @@ -1,53 +1,26 @@ import { Rest } from '@core/decorators/restaurant.decorator'; import { OwnerAuthGuard } from '@core/guards/auth.guard'; import { OwnerGuard } from '@core/guards/owner.guard'; +import { ProductService } from '@core/services/product.service'; import { PermAct, PermOwner } from '@core/services/role.service'; import { ProductVariant } from '@db/entities/owner/product-variant.entity'; import { Product } from '@db/entities/owner/product.entity'; -import { VariantGroup } from '@db/entities/owner/variant-group.entity'; -import { Variant } from '@db/entities/owner/variant.entity'; import { ProductTransformer } from '@db/transformers/product.transformer'; import { ValidationException } from '@lib/exceptions/validation.exception'; import { Validator } from '@lib/helpers/validator.helper'; import { Permissions } from '@lib/rbac'; import { BadRequestException, Body, Controller, Delete, Get, Param, Post, Res, UseGuards } from '@nestjs/common'; -import { In } from 'typeorm'; @Controller(':product_id/variants') @UseGuards(OwnerAuthGuard()) export class VariantController { + constructor(private productService: ProductService) {} + @Get() @UseGuards(OwnerGuard) @Permissions(`${PermOwner.Product}@${PermAct.R}`) async getVariants(@Rest() rest, @Res() response, @Param() param) { - const product = await Product.findOrFail({ where: { id: param.product_id, restaurant_id: rest.id } }); - const productVariants = await ProductVariant.findBy({ product_id: product.id }); - const variants = await Variant.find({ - where: { id: In(productVariants.map((val) => val.variant_id)) }, - order: { price: 'ASC' }, - }); - const groups = await VariantGroup.find({ - where: { id: In(variants.map((val) => val.group_id)) }, - order: { name: 'ASC' }, - }); - - const result: any[] = []; - - for (const group of groups) { - const payload = { - ...group, - variants: [], - }; - - for (const variant of variants) { - if (variant.group_id === group.id) { - payload.variants.push(variant); - } - } - - result.push(payload); - } - + const result = await this.productService.getVariants(param.product_id, rest); return response.data(result); } @@ -63,28 +36,7 @@ export class VariantController { throw new ValidationException(validation); } - const product = await Product.findOrFail({ where: { id: param.product_id, restaurant_id: rest.id } }); - const prodVariants: ProductVariant[] = []; - - const variants = await Variant.findBy({ id: In(body.variant_ids) }); - for (const variant of variants) { - const isExist = await ProductVariant.exists({ where: { product_id: product.id, variant_id: variant.id } }); - if (isExist) { - continue; - } - - const pcat = new ProductVariant(); - pcat.product_id = product.id; - pcat.variant_id = variant.id; - pcat.status = variant.status; - pcat.price = variant.price; - pcat.restaurant_id = rest.id; - prodVariants.push(pcat); - } - - if (prodVariants.length > 0) { - await ProductVariant.save(prodVariants); - } + const product = await this.productService.assignVariant(rest, param.product_id, body.variant_ids); await response.item(product, ProductTransformer); } diff --git a/src/app/owner/restaurant/restaurant.module.ts b/src/app/owner/restaurant/restaurant.module.ts index d1f2ab6..40aca99 100644 --- a/src/app/owner/restaurant/restaurant.module.ts +++ b/src/app/owner/restaurant/restaurant.module.ts @@ -1,6 +1,8 @@ import { Module } from '@nestjs/common'; import { OwnerCategoryModule } from './category/category.module'; import { OwnerLocationModule } from './location/location.module'; +import { OwnerNotificationModule } from './notification/notification.module'; +import { OwnerOrderModule } from './order/order.module'; import { OwnerProductModule } from './product/product.module'; import { RestaurantController } from './restaurant.controller'; import { OwnerStaffModule } from './staff/staff.module'; @@ -17,6 +19,8 @@ import { OwnerVariantModule } from './variant/variant.module'; OwnerVariantModule, OwnerProductModule, OwnerStockModule, + OwnerOrderModule, + OwnerNotificationModule, ], controllers: [RestaurantController], providers: [], diff --git a/src/app/owner/restaurant/table/table.controller.ts b/src/app/owner/restaurant/table/table.controller.ts index b55b6fc..51681df 100644 --- a/src/app/owner/restaurant/table/table.controller.ts +++ b/src/app/owner/restaurant/table/table.controller.ts @@ -1,22 +1,34 @@ import { Loc } from '@core/decorators/location.decorator'; import { Rest } from '@core/decorators/restaurant.decorator'; +import { Me } from '@core/decorators/user.decorator'; import { OwnerAuthGuard } from '@core/guards/auth.guard'; import { OwnerGuard } from '@core/guards/owner.guard'; +import { PdfService } from '@core/services/pdf.service'; import { PermAct, PermOwner } from '@core/services/role.service'; +import { UtilService } from '@core/services/util.service'; import { Location } from '@db/entities/owner/location.entity'; +import { Owner } from '@db/entities/owner/owner.entity'; import { Restaurant } from '@db/entities/owner/restaurant.entity'; import { Table, TableStatus } from '@db/entities/owner/table.entity'; import { TableTransformer } from '@db/transformers/table.transformer'; import { ValidationException } from '@lib/exceptions/validation.exception'; +import { config } from '@lib/helpers/config.helper'; +import { time } from '@lib/helpers/time.helper'; +import { writeFile } from '@lib/helpers/utils.helper'; import { Validator } from '@lib/helpers/validator.helper'; +import Logger from '@lib/logger/logger.library'; +import Socket, { PubSubEventType, PubSubPayloadType, PubSubStatus } from '@lib/pubsub/pubsub.lib'; import { Permissions } from '@lib/rbac'; import AppDataSource from '@lib/typeorm/datasource.typeorm'; +import { uuid } from '@lib/uid/uuid.library'; import { BadRequestException, Body, Controller, Get, Param, Post, Put, Res, UseGuards } from '@nestjs/common'; -import { Not } from 'typeorm'; +import { In, Not } from 'typeorm'; @Controller() @UseGuards(OwnerAuthGuard()) export class TableController { + constructor(private pdf: PdfService, private util: UtilService) {} + @Get() @UseGuards(OwnerGuard) @Permissions(`${PermOwner.Table}@${PermAct.R}`) @@ -74,7 +86,7 @@ export class TableController { @Put('/:table_id') @UseGuards(OwnerGuard) - @Permissions(`${PermOwner.Location}@${PermAct.U}`) + @Permissions(`${PermOwner.Table}@${PermAct.U}`) async update(@Rest() rest: Restaurant, @Body() body, @Res() response, @Param() param) { const rules = { number: 'required|unique|safe_text', @@ -107,4 +119,71 @@ export class TableController { return response.item(table, TableTransformer); } + + @Post('/label') + @UseGuards(OwnerGuard) + @Permissions(`${PermOwner.Table}@${PermAct.U}`) + async generateLabel(@Body() body, @Res() response, @Me() me: Owner, @Rest() restaurant: Restaurant) { + const rules = { + table_ids: 'required|array|uid', + }; + const validation = Validator.init(body, rules); + if (validation.fails()) { + throw new ValidationException(validation); + } + + const request_id = uuid(); + const processing = async (): Promise => { + const data = []; + const tables = await Table.find({ where: { id: In(body.table_ids) } }); + const locations = await Location.find({ where: { id: In(tables.map((val) => val.location_id)) } }); + for (const table of tables) { + const url = `${config.getAppURI()}/tables/${table.id}`; + const barcode = await this.util.getQrCode(url, { scale: 2 }); + const location = locations.find((val) => val.id === table.location_id); + data.push({ + name: table.number, + restaurant: restaurant.name, + location: location.name, + barcode: `data:image/png;base64,${barcode.toString('base64')}`, + }); + } + + const pdf = await this.pdf.tableLabel(data); + const dir = `${config.getPublicPath()}/files`; + const filename = `${time().unix()}-tables-label.pdf`; + return writeFile(dir, filename, pdf); + }; + + processing() + .then((path) => { + // send event + Socket.getInstance().event(me.id, { + request_id, + status: PubSubStatus.Success, + type: PubSubEventType.OwnerGetTableLabel, + payload: { + type: PubSubPayloadType.Download, + body: { + mime: 'text/href', + name: `${time().unix()}-tables-label.pdf`, + content: config.getDownloadURI(path), + }, + }, + }); + }) + .catch(async (error) => { + // send event + Socket.getInstance().event(me.id, { + request_id, + status: PubSubStatus.Fail, + type: PubSubEventType.OwnerGetTableLabel, + error: error.message, + }); + + Logger.getInstance().notify(error); + }); + + return response.data({ request_id }); + } } diff --git a/src/app/restaurant/menu.controller.ts b/src/app/restaurant/menu.controller.ts index 02b257a..18a40d2 100644 --- a/src/app/restaurant/menu.controller.ts +++ b/src/app/restaurant/menu.controller.ts @@ -1,5 +1,8 @@ +import { ProductStock } from '@db/entities/owner/product-stock.entity'; +import { ProductVariant } from '@db/entities/owner/product-variant.entity'; import { Product } from '@db/entities/owner/product.entity'; import { Restaurant } from '@db/entities/owner/restaurant.entity'; +import { Table } from '@db/entities/owner/table.entity'; import { ProductTransformer } from '@db/transformers/product.transformer'; import AppDataSource from '@lib/typeorm/datasource.typeorm'; import { Controller, Get, Param, Res } from '@nestjs/common'; @@ -9,12 +12,24 @@ export class MenuController { @Get() async getMenus(@Res() response, @Param() params) { const restaurant = await Restaurant.findOneByOrFail({ id: params.restaurant_id }); - const menus = await AppDataSource.createQueryBuilder(Product, 't1') - .where({ restaurant_id: restaurant.id }) - .search() - .sort() - .getPaged(); - await response.paginate(menus, ProductTransformer); + + let table: Table = null; + + const menus = AppDataSource.createQueryBuilder(Product, 't1') + .leftJoin(ProductVariant, 't2', 't2.product_id = t1.id') + .leftJoin(ProductStock, 't3', 't3.variant_id = t2.id') + .where('t1.restaurant_id = :restId', { restId: restaurant.id }); + + if (params.table_id) { + table = await Table.findOneBy({ id: params.table_id }); + + if (table) { + menus.andWhere('t3.location_id = :locId', { locId: table.location_id }); + } + } + + const results = await menus.search().sort().getPaged(); + await response.paginate(results, ProductTransformer); } @Get(':menu_id') diff --git a/src/app/staff/auth/auth.controller.ts b/src/app/staff/auth/auth.controller.ts new file mode 100644 index 0000000..dc8d8d1 --- /dev/null +++ b/src/app/staff/auth/auth.controller.ts @@ -0,0 +1,181 @@ +import { jwt } from '@config/jwt.config'; +import { StaffGuard } from '@core/guards/staff.guard'; +import { PermAct, PermStaff } from '@core/services/role.service'; +import { StaffBlacklist } from '@db/entities/staff/blacklist.entity'; +import { StaffSession } from '@db/entities/staff/session.entity'; +import { StaffUser } from '@db/entities/staff/user.entity'; +import { ValidationException } from '@lib/exceptions/validation.exception'; +import { config } from '@lib/helpers/config.helper'; +import { hash, hashAreEqual } from '@lib/helpers/encrypt.helper'; +import { time } from '@lib/helpers/time.helper'; +import { Validator } from '@lib/helpers/validator.helper'; +import { Permissions } from '@lib/rbac'; +import { uuid } from '@lib/uid/uuid.library'; +import { + BadRequestException, + Body, + Controller, + Delete, + Post, + Req, + Res, + UnauthorizedException, + UseGuards, +} from '@nestjs/common'; +import * as JWT from 'jsonwebtoken'; +import { ExtractJwt } from 'passport-jwt'; + +@Controller() +export class StaffAuthController { + // constructor(private mail: MailerService) {} + + static token(request: any): any { + try { + const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request); + const decoded = JWT.decode(token); + return { token, decoded }; + } catch (err) { + return null; + } + } + + @Post('/login') + async postLogin(@Body() body, @Res() response) { + const rules = { + username: 'required', + password: 'required', + }; + const validation = Validator.init({ ...body }, rules); + if (validation.fails()) { + throw new ValidationException(validation); + } + + const user = await StaffUser.findOne({ where: [{ email: body.username }, { phone: body.username }] }); + + if (!user) { + throw new UnauthorizedException('Email or phone number are not found'); + } + + if (!(await hashAreEqual(user.password, body.password))) { + throw new UnauthorizedException('Password is incorrect'); + } + + if (!user.isActive) { + throw new UnauthorizedException('Your account was inactive by system'); + } + + const tokenId = uuid(); + + const payload = { + email: user.email, + sub: user.id, + }; + const singOptions = { + jwtid: tokenId, + issuer: config.get('API_URI'), + audience: config.get('APP_URI'), //sha256(`${authSession.ip_address}_${authSession.user_agent}`), + ...jwt.signOptions, + }; + + return response.data({ + access_token: JWT.sign(payload, jwt.secret, singOptions), + }); + } + + @Delete('/logout') + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Profile}@${PermAct.D}`) + async deleteLogout(@Req() request, @Res() response) { + const token = StaffAuthController.token(request); + if (token) { + const authSession = await StaffSession.findOne({ + where: { + staff_user_id: token.decoded.sub, + token_id: token.decoded.jti, + }, + }); + + // if exists then set to logged out + if (authSession) { + authSession.token_deleted = true; + authSession.logged_out_at = time().toDate(); + await authSession.save(); + } else { + // if not, then blacklist token + await StaffBlacklist.store(token.token); + } + } + + return response.noContent(); + } + + @Post('/forgot-password') + async postForgotPass(@Body() { email }, @Res() response) { + const rules = { + email: 'required|email', + }; + const validation = Validator.init({ email }, rules); + if (validation.fails()) { + throw new ValidationException(validation); + } + + const staff: StaffUser = await StaffUser.findOne({ where: { email } }); + if (staff && staff.id) { + staff.reset_token = uuid(); + staff.reset_token_expires = time().add(24, 'hour').toDate(); + await staff.save(); + + // this.mail + // .sendMail({ + // to: staff.email, + // subject: 'Set up a new password', + // template: 'change-password', + // context: { + // name: staff.name, + // link: `${config.get('STAFF_URI')}/reset-password/${staff.reset_token}`, + // }, + // }) + // .then(() => null) + // .catch((error) => Logger.getInstance().notify(error)); + } + + return response.noContent(); + } + + @Post('/change-password') + async postChangePass(@Body() body, @Res() response) { + const rules = { + token: 'required', + password: 'required|confirmed|min:6', + }; + const validation = Validator.init(body, rules); + if (validation.fails()) { + throw new ValidationException(validation); + } + + const staff: StaffUser = await StaffUser.findOrFail({ where: { reset_token: body.token } }); + + if (time().toDate() > staff.reset_token_expires) { + throw new BadRequestException('This reset token has expired'); + } + + staff.password = await hash(body.password); + staff.reset_token = null; + staff.reset_token_expires = null; + await staff.save(); + + // await this.mail + // .sendMail({ + // to: staff.email, + // subject: 'Changed password', + // template: 'changed-password', + // context: { + // name: staff.name, + // }, + // }) + // .then(() => null) + // .catch((error) => Logger.getInstance().notify(error)); + + return response.noContent(); + } +} diff --git a/src/app/staff/auth/auth.module.ts b/src/app/staff/auth/auth.module.ts new file mode 100644 index 0000000..a5d0956 --- /dev/null +++ b/src/app/staff/auth/auth.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { StaffAuthController } from './auth.controller'; + +@Module({ + controllers: [StaffAuthController], +}) +export class StaffAuthModule {} diff --git a/src/app/staff/profile/profile.controller.ts b/src/app/staff/profile/profile.controller.ts new file mode 100644 index 0000000..b1ca5f5 --- /dev/null +++ b/src/app/staff/profile/profile.controller.ts @@ -0,0 +1,73 @@ +import { Me } from '@core/decorators/user.decorator'; +import { StaffAuthGuard } from '@core/guards/auth.guard'; +import { StaffGuard } from '@core/guards/staff.guard'; +import { AuthService } from '@core/services/auth.service'; +import { AwsService } from '@core/services/aws.service'; +import { PermAct, PermStaff } from '@core/services/role.service'; +import { Media } from '@db/entities/core/media.entity'; +import { StaffUser } from '@db/entities/staff/user.entity'; +import { StaffTransformer } from '@db/transformers/staff.transformer'; +import { ValidationException } from '@lib/exceptions/validation.exception'; +import { Validator } from '@lib/helpers/validator.helper'; +import { Permissions } from '@lib/rbac'; +import { BadRequestException, Body, Controller, Delete, Get, Param, Post, Put, Req, Res, UseGuards } from '@nestjs/common'; +import { isEmpty } from 'lodash'; + +@Controller() +@UseGuards(StaffAuthGuard()) +export class StaffProfileController { + constructor(private auth: AuthService, private aws: AwsService) {} + + @Get() + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Profile}@${PermAct.R}`) + async me(@Me() me: StaffUser, @Res() response) { + console.log(me); + return response.item(me, StaffTransformer); + } + + @Put() + @Permissions(`${PermStaff.Profile}@${PermAct.U}`) + async update(@Param() param, @Body() body, @Res() response, @Me() me: StaffUser) { + const rules = { + name: 'required|safe_text', + phone: 'required|phone', + }; + const validation = Validator.init(body, rules); + if (validation.fails()) { + throw new ValidationException(validation); + } + + me.name = body.name; + me.phone = body.phone; + await me.save(); + + await response.item(me, StaffTransformer); + } + + @Post('/avatar') + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Profile}@${PermAct.C}`) + async uploadAvatar(@Req() request, @Res() response, @Me() me: StaffUser) { + const file = await this.aws.uploadFile(request, response, 'image', { dynamicPath: `staff/${me.id}/avatar` }); + if (!file || isEmpty(file)) { + throw new BadRequestException('Unable to upload image'); + } + + if (await me.image) { + await this.aws.removeFile(await me.image); + } + + await Media.build(me, file); + + await response.item(me, StaffTransformer); + } + + @Delete('/avatar') + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Profile}@${PermAct.D}`) + async deleteAvatar(@Res() response, @Me() me: StaffUser) { + await Media.delete({ staff_user_id: me.id }); + return response.noContent(); + } +} diff --git a/src/app/staff/profile/profile.module.ts b/src/app/staff/profile/profile.module.ts new file mode 100644 index 0000000..492fb0d --- /dev/null +++ b/src/app/staff/profile/profile.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { StaffProfileController } from './profile.controller'; + +@Module({ + controllers: [StaffProfileController], +}) +export class StaffProfileModule {} diff --git a/src/app/staff/restaurant/category/category.controller.ts b/src/app/staff/restaurant/category/category.controller.ts new file mode 100644 index 0000000..d8a9e5f --- /dev/null +++ b/src/app/staff/restaurant/category/category.controller.ts @@ -0,0 +1,91 @@ +import { Rest } from '@core/decorators/restaurant.decorator'; +import { StaffAuthGuard } from '@core/guards/auth.guard'; +import { StaffGuard } from '@core/guards/staff.guard'; +import { PermAct, PermStaff } from '@core/services/role.service'; +import { Category } from '@db/entities/owner/category.entity'; +import { CategoryTransformer } from '@db/transformers/category.transformer'; +import { ValidationException } from '@lib/exceptions/validation.exception'; +import { Validator } from '@lib/helpers/validator.helper'; +import { Permissions } from '@lib/rbac'; +import AppDataSource from '@lib/typeorm/datasource.typeorm'; +import { BadRequestException, Body, Controller, Get, Param, Post, Put, Res, UseGuards } from '@nestjs/common'; +import { Not } from 'typeorm'; + +@Controller() +@UseGuards(StaffAuthGuard()) +export class CategoryController { + @Get() + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Category}@${PermAct.R}`) + async index(@Rest() rest, @Res() response) { + const categories = await AppDataSource.createQueryBuilder(Category, 't1') + .where({ restaurant_id: rest.id }) + .search() + .sort() + .getPaged(); + await response.paginate(categories, CategoryTransformer); + } + + @Get('/:category_id') + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Category}@${PermAct.R}`) + async show(@Rest() rest, @Res() response, @Param() param) { + const category = await Category.findOneByOrFail({ restaurant_id: rest.id, id: param.category_id }); + await response.item(category, CategoryTransformer); + } + + @Post() + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Category}@${PermAct.C}`) + async create(@Rest() rest, @Body() body, @Res() response) { + const rules = { + name: 'required|unique|safe_text', + }; + const validation = Validator.init(body, rules); + if (validation.fails()) { + throw new ValidationException(validation); + } + + const categoryExist = await Category.exists({ where: { name: body.name, restaurant_id: rest.id } }); + if (categoryExist) { + throw new BadRequestException('Category has already existed.'); + } + + const categ = new Category(); + categ.name = body.name; + categ.restaurant_id = rest.id; + await categ.save(); + + return response.item(categ, CategoryTransformer); + } + + @Put('/:category_id') + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Category}@${PermAct.U}`) + async update(@Rest() rest, @Body() body, @Res() response, @Param() param) { + const rules = { + name: 'required|unique|safe_text', + }; + const validation = Validator.init(body, rules); + if (validation.fails()) { + throw new ValidationException(validation); + } + + if (!param.category_id) { + throw new BadRequestException(); + } + + const categoryExist = await Category.exists({ + where: { name: body.name, restaurant_id: rest.id, id: Not(param.category_id) }, + }); + if (categoryExist) { + throw new BadRequestException('Category has already existed.'); + } + + const categ = await Category.findOneByOrFail({ id: param.category_id }); + categ.name = body.name; + await categ.save(); + + return response.item(categ, CategoryTransformer); + } +} diff --git a/src/app/staff/restaurant/category/category.module.ts b/src/app/staff/restaurant/category/category.module.ts new file mode 100644 index 0000000..2fbd496 --- /dev/null +++ b/src/app/staff/restaurant/category/category.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { CategoryController } from './category.controller'; + +@Module({ + imports: [], + controllers: [CategoryController], + providers: [], +}) +export class StaffCategoryModule {} diff --git a/src/app/staff/restaurant/notification/notification.controller.ts b/src/app/staff/restaurant/notification/notification.controller.ts new file mode 100644 index 0000000..49d9ae9 --- /dev/null +++ b/src/app/staff/restaurant/notification/notification.controller.ts @@ -0,0 +1,49 @@ +import { Loc } from '@core/decorators/location.decorator'; +import { Rest } from '@core/decorators/restaurant.decorator'; +import { StaffAuthGuard } from '@core/guards/auth.guard'; +import { StaffGuard } from '@core/guards/staff.guard'; +import { PermAct, PermStaff } from '@core/services/role.service'; +import { Notification } from '@db/entities/core/notification.entity'; +import { Location } from '@db/entities/owner/location.entity'; +import { Restaurant } from '@db/entities/owner/restaurant.entity'; +import { NotificationTransformer } from '@db/transformers/notification.transformer'; +import { ValidationException } from '@lib/exceptions/validation.exception'; +import { Validator } from '@lib/helpers/validator.helper'; +import { Permissions } from '@lib/rbac'; +import AppDataSource from '@lib/typeorm/datasource.typeorm'; +import { Body, Controller, Get, Put, Res, UseGuards } from '@nestjs/common'; +import { In } from 'typeorm'; + +@Controller() +@UseGuards(StaffAuthGuard()) +export class NotificationController { + @Get() + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Notification}@${PermAct.R}`) + async index(@Rest() rest: Restaurant, @Loc() loc: Location, @Res() response) { + const query = AppDataSource.createQueryBuilder(Notification, 't1').where({ restaurant_id: rest.id }); + + if (loc) { + query.andWhere('t1.location_id = :locId', { locId: loc.id }); + } + + const results = await query.search().sort().getPaged(); + await response.paginate(results, NotificationTransformer); + } + + @Put() + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Notification}@${PermAct.U}`) + async mark(@Body() body, @Res() response) { + const rules = { + ids: `required|array|uid`, + }; + const validation = Validator.init(body, rules); + if (validation.fails()) { + throw new ValidationException(validation); + } + + await Notification.update({ id: In(body.ids) }, { is_read: true }); + return response.noContent(); + } +} diff --git a/src/app/staff/restaurant/notification/notification.module.ts b/src/app/staff/restaurant/notification/notification.module.ts new file mode 100644 index 0000000..864f8e3 --- /dev/null +++ b/src/app/staff/restaurant/notification/notification.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { NotificationController } from './notification.controller'; + +@Module({ + imports: [], + controllers: [NotificationController], + providers: [], +}) +export class StafffNotificationModule {} diff --git a/src/app/staff/restaurant/order/detail.controller.ts b/src/app/staff/restaurant/order/detail.controller.ts new file mode 100644 index 0000000..bf58277 --- /dev/null +++ b/src/app/staff/restaurant/order/detail.controller.ts @@ -0,0 +1,172 @@ +import { Rest } from '@core/decorators/restaurant.decorator'; +import { Me } from '@core/decorators/user.decorator'; +import { StaffAuthGuard } from '@core/guards/auth.guard'; +import { StaffGuard } from '@core/guards/staff.guard'; +import { PermAct, PermStaff } from '@core/services/role.service'; +import { Notification, NotificationType } from '@db/entities/core/notification.entity'; +import { OrderProduct, OrderProductStatus } from '@db/entities/core/order-product.entity'; +import { Order, OrderStatus } from '@db/entities/core/order.entity'; +import { ProductStock } from '@db/entities/owner/product-stock.entity'; +import { Table, TableStatus } from '@db/entities/owner/table.entity'; +import { StaffUser } from '@db/entities/staff/user.entity'; +import { OrderTransformer } from '@db/transformers/order.transformer'; +import { GenericException } from '@lib/exceptions/generic.exception'; +import { ValidationException } from '@lib/exceptions/validation.exception'; +import { time } from '@lib/helpers/time.helper'; +import { Validator } from '@lib/helpers/validator.helper'; +import Socket from '@lib/pubsub/pubsub.lib'; +import { Permissions } from '@lib/rbac'; +import AppDataSource from '@lib/typeorm/datasource.typeorm'; +import { uuid } from '@lib/uid/uuid.library'; +import { Body, Controller, Get, Param, Put, Res, UseGuards } from '@nestjs/common'; +import { In } from 'typeorm'; + +@Controller(':order_id') +@UseGuards(StaffAuthGuard()) +export class DetailController { + static async action(order: Order, action: OrderStatus, actor: StaffUser) { + try { + switch (order.status) { + case OrderStatus.WaitingApproval: { + // @TODO: How "REJECTED" flow works? + if (![OrderStatus.Confirmed, OrderStatus.Cancelled].includes(action)) { + throw new GenericException(`Order ${order.number} must be confirmed first.`); + } + + // @TODO: Able to cancel/decline Product and Recalculate Gross Total + + order.status = action; + break; + } + case OrderStatus.Confirmed: { + if (![OrderStatus.Preparing].includes(action)) { + throw new GenericException(`Order ${order.number} can't be ${action}.`); + } + + // @TODO: Able to cancel/decline Product and Recalculate Gross Total + + order.status = OrderStatus.Preparing; + break; + } + case OrderStatus.Preparing: { + if (![OrderStatus.Served].includes(action)) { + throw new GenericException(`Order ${order.number} can't be ${action}.`); + } + + order.status = OrderStatus.Served; + break; + } + case OrderStatus.Served: { + if (![OrderStatus.WaitingPayment].includes(action)) { + throw new GenericException(`Order ${order.number} can't be ${action}.`); + } + + order.status = OrderStatus.WaitingPayment; + break; + } + case OrderStatus.WaitingPayment: { + if (![OrderStatus.Completed].includes(action)) { + throw new GenericException(`Order ${order.number} can't be ${action}.`); + } + + order.status = OrderStatus.Completed; + order.billed_at = time().toDate(); + break; + } + case OrderStatus.Completed: + // @TODO: Decrease stock + const orderProducts = await OrderProduct.findBy({ order_id: order.id }); + const productStocks = await ProductStock.findBy({ + variant_id: In(orderProducts.map((val) => val.product_variant_id)), + }); + + for (const stock of productStocks) { + const orderProduct = orderProducts.find((val) => val.product_variant_id === stock.variant_id); + if (orderProduct) { + stock.onhand -= orderProduct.qty; + stock.allocated -= orderProduct.qty; + stock.actor = actor ? actor.logName : 'System'; + stock.last_action = `Order ${order.number}`; + await AppDataSource.transaction(async (manager) => { + await manager.getRepository(ProductStock).save(stock); + }); + } + } + + break; + case OrderStatus.Cancelled: { + if (![OrderStatus.WaitingApproval].includes(action)) { + throw new GenericException(`Order ${order.number} can't be ${action}.`); + } + + order.status = OrderStatus.Cancelled; + break; + } + } + + const notification = new Notification(); + notification.title = 'Order Updated'; + notification.content = JSON.stringify(order); + notification.actor = 'System'; + notification.location_id = order.location_id; + notification.restaurant_id = order.restaurant_id; + notification.type = NotificationType.OrderUpdate; + notification.order_id = order.id; + + await AppDataSource.transaction(async (manager) => { + await manager.getRepository(Order).save(order); + + if ([OrderStatus.Cancelled].includes(order.status)) { + await manager.getRepository(OrderProduct).update({ order_id: order.id }, { status: OrderProductStatus.Cancelled }); + } + + if ([OrderStatus.Completed, OrderStatus.Cancelled].includes(order.status)) { + const table = await manager.getRepository(Table).findOneBy({ id: order.table_id }); + if (!table) { + throw new Error(`Table not found with ID: ${order.table_id}`); + } + table.status = TableStatus.Available; + await manager.getRepository(Table).save(table); + } + + await manager.getRepository(Notification).save(notification); + }); + + Socket.getInstance().notify(notification.order_id, { + request_id: uuid(), + data: notification, + }); + } catch (error) { + throw error; + } + } + + @Get() + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Order}@${PermAct.R}`) + async show(@Rest() rest, @Res() response, @Param() param) { + const order = await Order.findOneByOrFail({ restaurant_id: rest.id, id: param.order_id }); + await response.item(order, OrderTransformer); + } + + @Put() + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Order}@${PermAct.U}`) + async action(@Body() body, @Param() param, @Res() response, @Me() me: StaffUser) { + const rules = { + action: `required|in:${Object.values(OrderStatus) + .filter((val) => val !== 'waiting_approval') + .join(',')}`, + }; + const validation = Validator.init(body, rules); + if (validation.fails()) { + throw new ValidationException(validation); + } + + const order = await Order.findOneByOrFail({ id: param.order_id }); + await DetailController.action(order, body.action, me); + await order.reload(); + + return response.item(order, OrderTransformer); + } +} diff --git a/src/app/staff/restaurant/order/order.controller.ts b/src/app/staff/restaurant/order/order.controller.ts new file mode 100644 index 0000000..d554b80 --- /dev/null +++ b/src/app/staff/restaurant/order/order.controller.ts @@ -0,0 +1,35 @@ +import { Loc } from '@core/decorators/location.decorator'; +import { Quero } from '@core/decorators/quero.decorator'; +import { Rest } from '@core/decorators/restaurant.decorator'; +import { StaffAuthGuard } from '@core/guards/auth.guard'; +import { StaffGuard } from '@core/guards/staff.guard'; +import { PermAct, PermStaff } from '@core/services/role.service'; +import { Order } from '@db/entities/core/order.entity'; +import { Location } from '@db/entities/owner/location.entity'; +import { Restaurant } from '@db/entities/owner/restaurant.entity'; +import { OrderTransformer } from '@db/transformers/order.transformer'; +import { Permissions } from '@lib/rbac'; +import AppDataSource from '@lib/typeorm/datasource.typeorm'; +import { Controller, Get, Res, UseGuards } from '@nestjs/common'; + +@Controller() +@UseGuards(StaffAuthGuard()) +export class OrderController { + @Get() + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Order}@${PermAct.R}`) + async index(@Rest() rest: Restaurant, @Loc() loc: Location, @Res() response, @Quero() quero) { + const query = AppDataSource.createQueryBuilder(Order, 't1').where({ restaurant_id: rest.id }); + + if (loc) { + query.andWhere('t1.location_id = :locId', { locId: loc.id }); + } + + if (quero.status) { + query.andWhere('t1.status = :status', { status: quero.status }); + } + + const results = await query.search().sort().getPaged(); + await response.paginate(results, OrderTransformer); + } +} diff --git a/src/app/staff/restaurant/order/order.module.ts b/src/app/staff/restaurant/order/order.module.ts new file mode 100644 index 0000000..19a7312 --- /dev/null +++ b/src/app/staff/restaurant/order/order.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { DetailController } from './detail.controller'; +import { OrderController } from './order.controller'; + +@Module({ + imports: [], + controllers: [OrderController, DetailController], + providers: [], +}) +export class StaffOrderModule {} diff --git a/src/app/staff/restaurant/product/category.controller.ts b/src/app/staff/restaurant/product/category.controller.ts new file mode 100644 index 0000000..1132480 --- /dev/null +++ b/src/app/staff/restaurant/product/category.controller.ts @@ -0,0 +1,51 @@ +import { Rest } from '@core/decorators/restaurant.decorator'; +import { StaffAuthGuard } from '@core/guards/auth.guard'; +import { StaffGuard } from '@core/guards/staff.guard'; +import { ProductService } from '@core/services/product.service'; +import { PermAct, PermStaff } from '@core/services/role.service'; +import { ProductCategory } from '@db/entities/owner/product-category.entity'; +import { Product } from '@db/entities/owner/product.entity'; +import { ProductTransformer } from '@db/transformers/product.transformer'; +import { ValidationException } from '@lib/exceptions/validation.exception'; +import { Validator } from '@lib/helpers/validator.helper'; +import { Permissions } from '@lib/rbac'; +import { BadRequestException, Body, Controller, Delete, Param, Post, Res, UseGuards } from '@nestjs/common'; + +@Controller(':product_id') +@UseGuards(StaffAuthGuard()) +export class CategoryController { + constructor(private productService: ProductService) {} + + @Post('/categories') + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Product}@${PermAct.U}`) + async addCategory(@Body() body, @Res() response, @Param() param, @Rest() rest) { + const rules = { + category_ids: 'required|array|uid', + }; + const validation = Validator.init(body, rules); + if (validation.fails()) { + throw new ValidationException(validation); + } + + const product = await this.productService.assignCategories(rest, param.product_id, body.category_ids); + + await response.item(product, ProductTransformer); + } + + @Delete('/categories/:category_id') + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Product}@${PermAct.D}`) + async deleteCategory(@Param() param, @Res() response, @Rest() rest) { + if (!param.category_id) { + throw new BadRequestException(); + } + + const product = await Product.findOrFail({ where: { id: param.product_id, restaurant_id: rest.id } }); + const category = await ProductCategory.findOneByOrFail({ id: param.category_id, product_id: product.id }); + + await category.remove(); + + return response.noContent(); + } +} diff --git a/src/app/staff/restaurant/product/detail.controller.ts b/src/app/staff/restaurant/product/detail.controller.ts new file mode 100644 index 0000000..2219daf --- /dev/null +++ b/src/app/staff/restaurant/product/detail.controller.ts @@ -0,0 +1,116 @@ +import { Quero } from '@core/decorators/quero.decorator'; +import { Rest } from '@core/decorators/restaurant.decorator'; +import { StaffAuthGuard } from '@core/guards/auth.guard'; +import { StaffGuard } from '@core/guards/staff.guard'; +import { AwsService } from '@core/services/aws.service'; +import { PermAct, PermStaff } from '@core/services/role.service'; +import { Media } from '@db/entities/core/media.entity'; +import { ProductStock } from '@db/entities/owner/product-stock.entity'; +import { Product } from '@db/entities/owner/product.entity'; +import { ProductTransformer } from '@db/transformers/product.transformer'; +import { StockTransformer } from '@db/transformers/stock.transformer'; +import { ValidationException } from '@lib/exceptions/validation.exception'; +import { Validator } from '@lib/helpers/validator.helper'; +import { Permissions } from '@lib/rbac'; +import { BadRequestException, Body, Controller, Delete, Get, Param, Post, Put, Req, Res, UseGuards } from '@nestjs/common'; +import { Not } from 'typeorm'; + +@Controller(':product_id') +@UseGuards(StaffAuthGuard()) +export class DetailController { + constructor(private aws: AwsService) {} + + @Get() + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Product}@${PermAct.R}`) + async show(@Rest() rest, @Res() response, @Param() param) { + const product = await Product.findOneByOrFail({ restaurant_id: rest.id, id: param.product_id }); + await response.item(product, ProductTransformer); + } + + @Put() + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Product}@${PermAct.C}`) + async update(@Rest() rest, @Body() body, @Res() response, @Param() param) { + const rules = { + sku: 'required|sku', + name: 'required|string', + description: 'string', + price: 'required|numeric|min:0', + }; + const validation = Validator.init(body, rules); + if (validation.fails()) { + throw new ValidationException(validation); + } + + if (!param.product_id) { + throw new BadRequestException(); + } + + const productExist = await Product.exists({ + where: { sku: body.sku, restaurant_id: rest.id, id: Not(param.product_id) }, + }); + if (productExist) { + throw new BadRequestException('Product has already existed.'); + } + + const product = await Product.findOneByOrFail({ id: param.product_id }); + product.sku = body.sku; + product.name = body.name; + product.description = body.decription; + product.price = body.price; + await product.save(); + + return response.item(product, ProductTransformer); + } + + @Post('/images') + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Product}@${PermAct.U}`) + async uploadImage(@Param() param, @Req() request, @Res() response, @Rest() rest) { + const product = await Product.findOrFail({ where: { id: param.product_id, restaurant_id: rest.id } }); + if ((await Media.total(product)) >= 5) { + throw new BadRequestException('Maximum total image allowed is 5'); + } + + const file = await this.aws.uploadFile(request, response, 'image', { + dynamicPath: `restaurants/${rest.id}/products`, + }); + + await Media.add(product, file); + + await response.item(product, ProductTransformer); + } + + @Delete('/images/:image_id') + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Product}@${PermAct.D}`) + async deleteImage(@Param() param, @Res() response, @Rest() rest) { + const product = await Product.findOrFail({ where: { id: param.product_id, restaurant_id: rest.id } }); + const media = await Media.findOrFail({ where: { id: param.image_id, product_id: product.id } }); + + await this.aws.removeFile(media); + + return response.noContent(); + } + + @Get('/stocks') + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Product}@${PermAct.R}`) + async getStocks(@Param() param, @Res() response, @Rest() rest, @Quero() quero) { + if (!param.product_id) { + throw new BadRequestException(); + } + + const product = await Product.findOrFail({ where: { id: param.product_id, restaurant_id: rest.id } }); + const where = { product_id: product.id }; + + if (quero.location_id) { + Object.assign(where, { ...where, location_id: quero.location_id }); + } + + const stocks = await ProductStock.findBy(where); + + return response.collection(stocks, StockTransformer); + } +} diff --git a/src/app/staff/restaurant/product/product.controller.ts b/src/app/staff/restaurant/product/product.controller.ts new file mode 100644 index 0000000..e246bbe --- /dev/null +++ b/src/app/staff/restaurant/product/product.controller.ts @@ -0,0 +1,58 @@ +import { Rest } from '@core/decorators/restaurant.decorator'; +import { StaffAuthGuard } from '@core/guards/auth.guard'; +import { StaffGuard } from '@core/guards/staff.guard'; +import { ProductService } from '@core/services/product.service'; +import { PermAct, PermStaff } from '@core/services/role.service'; +import { Product, ProductStatus } from '@db/entities/owner/product.entity'; +import { ProductTransformer } from '@db/transformers/product.transformer'; +import { ValidationException } from '@lib/exceptions/validation.exception'; +import { Validator } from '@lib/helpers/validator.helper'; +import { Permissions } from '@lib/rbac'; +import AppDataSource from '@lib/typeorm/datasource.typeorm'; +import { BadRequestException, Body, Controller, Get, Post, Res, UseGuards } from '@nestjs/common'; + +@Controller() +@UseGuards(StaffAuthGuard()) +export class ProductController { + constructor(private productService: ProductService) {} + + @Get() + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Product}@${PermAct.R}`) + async index(@Rest() rest, @Res() response) { + const products = await AppDataSource.createQueryBuilder(Product, 't1') + .where({ restaurant_id: rest.id }) + .search() + .sort() + .getPaged(); + await response.paginate(products, ProductTransformer); + } + + @Post() + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Product}@${PermAct.C}`) + async create(@Rest() rest, @Body() body, @Res() response) { + const rules = { + sku: 'required|sku', + name: 'required|string', + description: 'string', + price: 'required|numeric|min:0', + status: `required|in:${Object.values(ProductStatus).join(',')}`, + category_ids: 'array|unique|uid', + variant_ids: 'array|unique|uid', + }; + const validation = Validator.init(body, rules); + if (validation.fails()) { + throw new ValidationException(validation); + } + + const productExist = await Product.exists({ where: { sku: body.sku, restaurant_id: rest.id } }); + if (productExist) { + throw new BadRequestException('Product SKU has already existed.'); + } + + const prod = await this.productService.create(rest, body); + + return response.item(prod, ProductTransformer); + } +} diff --git a/src/app/staff/restaurant/product/product.module.ts b/src/app/staff/restaurant/product/product.module.ts new file mode 100644 index 0000000..ed9ce99 --- /dev/null +++ b/src/app/staff/restaurant/product/product.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { CategoryController } from './category.controller'; +import { DetailController } from './detail.controller'; +import { ProductController } from './product.controller'; +import { VariantController } from './variant.controller'; + +@Module({ + imports: [], + controllers: [ProductController, DetailController, CategoryController, VariantController], + providers: [], +}) +export class StaffProductModule {} diff --git a/src/app/staff/restaurant/product/variant.controller.ts b/src/app/staff/restaurant/product/variant.controller.ts new file mode 100644 index 0000000..6e1a2a8 --- /dev/null +++ b/src/app/staff/restaurant/product/variant.controller.ts @@ -0,0 +1,59 @@ +import { Rest } from '@core/decorators/restaurant.decorator'; +import { StaffAuthGuard } from '@core/guards/auth.guard'; +import { StaffGuard } from '@core/guards/staff.guard'; +import { ProductService } from '@core/services/product.service'; +import { PermAct, PermStaff } from '@core/services/role.service'; +import { ProductVariant } from '@db/entities/owner/product-variant.entity'; +import { Product } from '@db/entities/owner/product.entity'; +import { ProductTransformer } from '@db/transformers/product.transformer'; +import { ValidationException } from '@lib/exceptions/validation.exception'; +import { Validator } from '@lib/helpers/validator.helper'; +import { Permissions } from '@lib/rbac'; +import { BadRequestException, Body, Controller, Delete, Get, Param, Post, Res, UseGuards } from '@nestjs/common'; + +@Controller(':product_id/variants') +@UseGuards(StaffAuthGuard()) +export class VariantController { + constructor(private productService: ProductService) {} + + @Get() + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Product}@${PermAct.R}`) + async getVariants(@Rest() rest, @Res() response, @Param() param) { + const result = await this.productService.getVariants(param.product_id, rest); + return response.data(result); + } + + @Post() + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Product}@${PermAct.U}`) + async addVariant(@Body() body, @Res() response, @Param() param, @Rest() rest) { + const rules = { + variant_ids: 'required|array', + }; + const validation = Validator.init(body, rules); + if (validation.fails()) { + throw new ValidationException(validation); + } + + const product = await this.productService.assignVariant(rest, param.product_id, body.variant_ids); + + await response.item(product, ProductTransformer); + } + + @Delete('/:variant_id') + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Product}@${PermAct.D}`) + async deleteVariant(@Param() param, @Res() response, @Rest() rest) { + if (!param.variant_id) { + throw new BadRequestException(); + } + + const product = await Product.findOrFail({ where: { id: param.product_id, restaurant_id: rest.id } }); + const variant = await ProductVariant.findOneByOrFail({ id: param.variant_id, product_id: product.id }); + + await variant.remove(); + + return response.noContent(); + } +} diff --git a/src/app/staff/restaurant/restaurant.controller.ts b/src/app/staff/restaurant/restaurant.controller.ts new file mode 100644 index 0000000..4bb895f --- /dev/null +++ b/src/app/staff/restaurant/restaurant.controller.ts @@ -0,0 +1,18 @@ +import { Rest } from '@core/decorators/restaurant.decorator'; +import { StaffAuthGuard } from '@core/guards/auth.guard'; +import { StaffGuard } from '@core/guards/staff.guard'; +import { PermAct, PermStaff } from '@core/services/role.service'; +import { RestaurantTransformer } from '@db/transformers/restaurant.transformer'; +import { Permissions } from '@lib/rbac'; +import { Controller, Get, Res, UseGuards } from '@nestjs/common'; + +@Controller() +@UseGuards(StaffAuthGuard()) +export class StaffRestaurantController { + @Get() + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Restaurant}@${PermAct.R}`) + async index(@Rest() rest, @Res() response) { + return response.item(rest, RestaurantTransformer); + } +} diff --git a/src/app/staff/restaurant/restaurant.module.ts b/src/app/staff/restaurant/restaurant.module.ts new file mode 100644 index 0000000..98767fb --- /dev/null +++ b/src/app/staff/restaurant/restaurant.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { StafffNotificationModule } from './notification/notification.module'; +import { StaffOrderModule } from './order/order.module'; +import { StaffRestaurantController } from './restaurant.controller'; + +@Module({ + imports: [StaffOrderModule, StafffNotificationModule], + controllers: [StaffRestaurantController], + providers: [], +}) +export class StaffRestaurantModule {} diff --git a/src/app/staff/restaurant/stock/stock.controller.ts b/src/app/staff/restaurant/stock/stock.controller.ts new file mode 100644 index 0000000..ac37403 --- /dev/null +++ b/src/app/staff/restaurant/stock/stock.controller.ts @@ -0,0 +1,250 @@ +import { Loc } from '@core/decorators/location.decorator'; +import { Rest } from '@core/decorators/restaurant.decorator'; +import { Me } from '@core/decorators/user.decorator'; +import { StaffAuthGuard } from '@core/guards/auth.guard'; +import { StaffGuard } from '@core/guards/staff.guard'; +import { PermAct, PermStaff } from '@core/services/role.service'; +import { Location } from '@db/entities/owner/location.entity'; +import { ProductStock } from '@db/entities/owner/product-stock.entity'; +import { ProductVariant } from '@db/entities/owner/product-variant.entity'; +import { Product, ProductStatus } from '@db/entities/owner/product.entity'; +import { VariantStatus } from '@db/entities/owner/variant.entity'; +import { StaffUser } from '@db/entities/staff/user.entity'; +import { StockTransformer } from '@db/transformers/stock.transformer'; +import { ValidationException } from '@lib/exceptions/validation.exception'; +import { Validator } from '@lib/helpers/validator.helper'; +import Socket, { PubSubEventType, PubSubPayloadType, PubSubStatus } from '@lib/pubsub/pubsub.lib'; +import { Permissions } from '@lib/rbac'; +import AppDataSource from '@lib/typeorm/datasource.typeorm'; +import { uuid } from '@lib/uid/uuid.library'; +import { + BadRequestException, + Body, + Controller, + Get, + NotFoundException, + Param, + Post, + Put, + Res, + UseGuards, +} from '@nestjs/common'; +import { get } from 'lodash'; +import { In, IsNull } from 'typeorm'; + +@Controller() +@UseGuards(StaffAuthGuard()) +export class StockController { + @Get() + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Stock}@${PermAct.R}`) + async index(@Rest() rest, @Res() response, @Loc() loc: Location) { + const query = AppDataSource.createQueryBuilder(ProductStock, 't1').where({ + restaurant_id: rest.id, + }); + + if (loc) { + query.andWhere({ location_id: loc.id }); + } + + const stocks = await query.search().sort().getPaged(); + await response.paginate(stocks, StockTransformer); + } + + @Get('/:stock_id') + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Stock}@${PermAct.R}`) + async show(@Rest() rest, @Res() response, @Param() param) { + const stock = await ProductStock.findOneByOrFail({ + restaurant_id: rest.id, + id: param.stock_id, + }); + await response.item(stock, StockTransformer); + } + + @Post() + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Stock}@${PermAct.C}`) + async create(@Rest() rest, @Body() body, @Res() response, @Me() me: StaffUser) { + const rules = { + products: 'required|array', + location_ids: 'required|array|uid', + }; + const validation = Validator.init(body, rules); + if (validation.fails()) { + throw new ValidationException(validation); + } + + const request_id = uuid(); + + const progress = async () => { + const stats = { success: [], fails: [] }; + + try { + const items = await Product.findBy({ + id: In(body.products.map((val) => val.id)), + restaurant_id: rest.id, + }); + + if (!items.length) { + throw new BadRequestException(`There is no products found`); + } + + const locations = await Location.findBy({ + id: In(body.location_ids), + restaurant_id: rest.id, + }); + + if (!locations.length) { + throw new BadRequestException(`There is no locations found`); + } + + const stocks: ProductStock[] = []; + + for (const item of body.products) { + const product = items.find((val) => val.id === item.id); + + const where = { product_id: product.id }; + + if (item.variant_id) { + Object.assign(where, { ...where, variant_id: item.variant_id }); + } + + const productVariant = await ProductVariant.findOneBy(where); + + if (!productVariant) { + throw new NotFoundException(`Can't found ${product.sku} match with ID Variant ${item.variant_id}`); + } + + for (const location of locations) { + try { + const isExist = await ProductStock.exists({ + where: { + location_id: location.id, + product_id: product.id, + variant_id: productVariant.id, + }, + }); + + const productFullName = await productVariant.getFullName(); + + if (product.status == ProductStatus.Discontinued) { + throw new BadRequestException(`Can't add initial stock, ${productFullName} status is discontinued`); + } + + if (isExist) { + throw new BadRequestException(`Product ${productFullName} already exist at ${location.name}`); + } + + // Count Product Variants (to check if it has parent, so it should be skipped) + const countVariant = await ProductVariant.count({ + where: { product_id: product.id }, + }); + if (countVariant > 1) { + const check = await ProductVariant.exists({ + where: { id: productVariant.id, variant_id: IsNull() }, + }); + if (check) { + // Skip if it's a Parent + throw new BadRequestException( + `Product ${productFullName} are skipped because it has variants. Please choose the variant.` + ); + } + } + + const quantity = body.products.find((val) => val.id === product.id); + + const action = `Initial Stock: ${productFullName} at ${location.name}`; + const productStock = new ProductStock(); + productStock.product_id = productVariant.product_id; + productStock.variant_id = productVariant.id; + productStock.location_id = location.id; + productStock.onhand = get(quantity, 'qty', 0); + productStock.restaurant_id = rest.id; + productStock.last_action = action; + productStock.actor = me.logName; + stocks.push(productStock); + + stats.success.push(`Product ${productFullName} has been added to ${location.name}`); + } catch (error) { + stats.fails.push(error.message); + } + } + } + + if (stocks.length > 0) { + await ProductStock.save(stocks); + } + } catch (error) { + stats.fails.push(error.message); + } + + return stats; + }; + + progress() + .then(({ success, fails }) => { + const payload = [...fails, ...success]; + + Socket.getInstance().event(me.id, { + request_id, + status: fails.length > 0 || !success.length ? PubSubStatus.Warning : PubSubStatus.Success, + type: PubSubEventType.StaffCreateStock, + payload: { + type: PubSubPayloadType.Dialog, + body: payload.length > 50 ? [...payload.slice(0, 50), 'and more...'] : payload, + }, + }); + }) + .catch((error) => { + Socket.getInstance() + .event(me.id, { + request_id, + status: PubSubStatus.Fail, + type: PubSubEventType.StaffCreateStock, + error: error.message, + }) + .catch((error) => console.log(error)); + }); + + return response.data({ request_id }); + } + + @Put('/:stock_id') + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Stock}@${PermAct.U}`) + async update(@Rest() rest, @Body() body, @Res() response, @Param() param) { + const rules = { + onhand: 'required|numeric|min:0', + }; + const validation = Validator.init(body, rules); + if (validation.fails()) { + throw new ValidationException(validation); + } + + const productStock = await ProductStock.findOneByOrFail({ + restaurant_id: rest.id, + id: param.stock_id, + }); + const product = await productStock.product; + const variant = await productStock.variant; + + // @TODO: How about changing parent stock that affected to their variants stock + + if (variant === null) { + // Set all product variants to unavailable when parent are 0 + if (Number(body.onhand) <= 0) { + await ProductStock.update({ product_id: product.id }, { onhand: 0 }); + await ProductVariant.update({ product_id: product.id }, { status: VariantStatus.Unvailable }); + + product.status = ProductStatus.Unvailable; + await product.save(); + } + } else { + productStock.onhand = Number(body.onhand); + await productStock.save(); + } + + await response.item(productStock, StockTransformer); + } +} diff --git a/src/app/staff/restaurant/stock/stock.module.ts b/src/app/staff/restaurant/stock/stock.module.ts new file mode 100644 index 0000000..5cfe886 --- /dev/null +++ b/src/app/staff/restaurant/stock/stock.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { StockController } from './stock.controller'; + +@Module({ + imports: [], + controllers: [StockController], + providers: [], +}) +export class StaffStockModule {} diff --git a/src/app/staff/restaurant/table/table.controller.ts b/src/app/staff/restaurant/table/table.controller.ts new file mode 100644 index 0000000..15c0d99 --- /dev/null +++ b/src/app/staff/restaurant/table/table.controller.ts @@ -0,0 +1,189 @@ +import { Loc } from '@core/decorators/location.decorator'; +import { Rest } from '@core/decorators/restaurant.decorator'; +import { Me } from '@core/decorators/user.decorator'; +import { StaffAuthGuard } from '@core/guards/auth.guard'; +import { StaffGuard } from '@core/guards/staff.guard'; +import { PdfService } from '@core/services/pdf.service'; +import { PermAct, PermStaff } from '@core/services/role.service'; +import { UtilService } from '@core/services/util.service'; +import { Location } from '@db/entities/owner/location.entity'; +import { Restaurant } from '@db/entities/owner/restaurant.entity'; +import { Table, TableStatus } from '@db/entities/owner/table.entity'; +import { StaffUser } from '@db/entities/staff/user.entity'; +import { TableTransformer } from '@db/transformers/table.transformer'; +import { ValidationException } from '@lib/exceptions/validation.exception'; +import { config } from '@lib/helpers/config.helper'; +import { time } from '@lib/helpers/time.helper'; +import { writeFile } from '@lib/helpers/utils.helper'; +import { Validator } from '@lib/helpers/validator.helper'; +import Logger from '@lib/logger/logger.library'; +import Socket, { PubSubEventType, PubSubPayloadType, PubSubStatus } from '@lib/pubsub/pubsub.lib'; +import { Permissions } from '@lib/rbac'; +import AppDataSource from '@lib/typeorm/datasource.typeorm'; +import { uuid } from '@lib/uid/uuid.library'; +import { BadRequestException, Body, Controller, Get, Param, Post, Put, Res, UseGuards } from '@nestjs/common'; +import { In, Not } from 'typeorm'; + +@Controller() +@UseGuards(StaffAuthGuard()) +export class TableController { + constructor(private pdf: PdfService, private util: UtilService) {} + + @Get() + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Table}@${PermAct.R}`) + async index(@Rest() rest, @Res() response, @Loc() loc: Location) { + const tables = AppDataSource.createQueryBuilder(Table, 't1'); + tables.where({ restaurant_id: rest.id }); + + if (loc && loc.id) { + tables.andWhere({ location_id: loc.id }); + } + + const data = await tables.search().sort().getPaged(); + + await response.paginate(data, TableTransformer); + } + + @Get('/:table_id') + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Table}@${PermAct.R}`) + async show(@Rest() rest, @Res() response, @Param() param) { + const table = await Table.findOneByOrFail({ restaurant_id: rest.id, id: param.table_id }); + await response.item(table, TableTransformer); + } + + @Post() + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Table}@${PermAct.C}`) + async create(@Rest() rest: Restaurant, @Body() body, @Res() response) { + const rules = { + number: 'required|unique|safe_text', + location_id: 'required', + status: `required|in:${Object.values(TableStatus).join(',')}`, + }; + const validation = Validator.init(body, rules); + if (validation.fails()) { + throw new ValidationException(validation); + } + + const loc = await Location.findOneByOrFail({ id: body.location_id }); + + const tableExist = await Table.exists({ where: { number: body.number, restaurant_id: rest.id, location_id: loc.id } }); + if (tableExist) { + throw new BadRequestException('Table has already existed.'); + } + + const table = new Table(); + table.number = body.number; + table.status = body.status; + table.location_id = loc.id; + table.restaurant_id = rest.id; + await table.save(); + + return response.item(table, TableTransformer); + } + + @Put('/:table_id') + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Table}@${PermAct.U}`) + async update(@Rest() rest: Restaurant, @Body() body, @Res() response, @Param() param) { + const rules = { + number: 'required|unique|safe_text', + location_id: 'required', + status: `required|in:${Object.values(TableStatus).join(',')}`, + }; + const validation = Validator.init(body, rules); + if (validation.fails()) { + throw new ValidationException(validation); + } + + if (!param.table_id) { + throw new BadRequestException(); + } + + const table = await Table.findOneByOrFail({ id: param.table_id }); + + const loc = await Location.findOneByOrFail({ id: table.location_id }); + + const tableExist = await Table.exists({ + where: { number: body.number, restaurant_id: rest.id, location_id: loc.id, id: Not(table.id) }, + }); + if (tableExist) { + throw new BadRequestException('Table has already existed.'); + } + + table.number = body.number; + table.status = body.status; + await table.save(); + + return response.item(table, TableTransformer); + } + + @Post('/label') + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Table}@${PermAct.U}`) + async generateLabel(@Body() body, @Res() response, @Me() me: StaffUser, @Rest() restaurant: Restaurant) { + const rules = { + table_ids: 'required|array|uid', + }; + const validation = Validator.init(body, rules); + if (validation.fails()) { + throw new ValidationException(validation); + } + + const request_id = uuid(); + const processing = async (): Promise => { + const data = []; + const tables = await Table.find({ where: { id: In(body.table_ids) } }); + const locations = await Location.find({ where: { id: In(tables.map((val) => val.location_id)) } }); + for (const table of tables) { + const url = `${config.getAppURI()}/tables/${table.id}`; + const barcode = await this.util.getQrCode(url, { scale: 2 }); + const location = locations.find((val) => val.id === table.location_id); + data.push({ + name: table.number, + restaurant: restaurant.name, + location: location.name, + barcode: `data:image/png;base64,${barcode.toString('base64')}`, + }); + } + + const pdf = await this.pdf.tableLabel(data); + const dir = `${config.getPublicPath()}/files`; + const filename = `${time().unix()}-tables-label.pdf`; + return writeFile(dir, filename, pdf); + }; + + processing() + .then((path) => { + // send event + Socket.getInstance().event(me.id, { + request_id, + status: PubSubStatus.Success, + type: PubSubEventType.StaffGetTableLabel, + payload: { + type: PubSubPayloadType.Download, + body: { + mime: 'text/href', + name: `${time().unix()}-tables-label.pdf`, + content: config.getDownloadURI(path), + }, + }, + }); + }) + .catch(async (error) => { + // send event + Socket.getInstance().event(me.id, { + request_id, + status: PubSubStatus.Fail, + type: PubSubEventType.StaffGetTableLabel, + error: error.message, + }); + + Logger.getInstance().notify(error); + }); + + return response.data({ request_id }); + } +} diff --git a/src/app/staff/restaurant/table/table.module.ts b/src/app/staff/restaurant/table/table.module.ts new file mode 100644 index 0000000..6a06566 --- /dev/null +++ b/src/app/staff/restaurant/table/table.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { TableController } from './table.controller'; + +@Module({ + imports: [], + controllers: [TableController], + providers: [], +}) +export class StaffTableModule {} diff --git a/src/app/staff/restaurant/variant/group.controller.ts b/src/app/staff/restaurant/variant/group.controller.ts new file mode 100644 index 0000000..2319508 --- /dev/null +++ b/src/app/staff/restaurant/variant/group.controller.ts @@ -0,0 +1,100 @@ +import { Rest } from '@core/decorators/restaurant.decorator'; +import { StaffAuthGuard } from '@core/guards/auth.guard'; +import { StaffGuard } from '@core/guards/staff.guard'; +import { PermAct, PermStaff } from '@core/services/role.service'; +import { VariantGroup, VariantGroupType } from '@db/entities/owner/variant-group.entity'; +import { VariantGroupTransformer } from '@db/transformers/variant-group.transformer'; +import { ValidationException } from '@lib/exceptions/validation.exception'; +import { isTrue } from '@lib/helpers/utils.helper'; +import { Validator } from '@lib/helpers/validator.helper'; +import { Permissions } from '@lib/rbac'; +import AppDataSource from '@lib/typeorm/datasource.typeorm'; +import { BadRequestException, Body, Controller, Get, Param, Post, Put, Res, UseGuards } from '@nestjs/common'; +import { Not } from 'typeorm'; + +@Controller('groups') +@UseGuards(StaffAuthGuard()) +export class VariantGroupController { + @Get() + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Variant}@${PermAct.R}`) + async index(@Rest() rest, @Res() response) { + const groups = await AppDataSource.createQueryBuilder(VariantGroup, 't1') + .where({ restaurant_id: rest.id }) + .search() + .sort() + .getPaged(); + await response.paginate(groups, VariantGroupTransformer); + } + + @Get('/:group_id') + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Variant}@${PermAct.R}`) + async show(@Rest() rest, @Res() response, @Param() param) { + const group = await VariantGroup.findOneByOrFail({ restaurant_id: rest.id, id: param.group_id }); + await response.item(group, VariantGroupTransformer); + } + + @Post() + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Variant}@${PermAct.C}`) + async create(@Rest() rest, @Body() body, @Res() response) { + const rules = { + name: 'required|unique|safe_text', + type: `required|in:${Object.values(VariantGroupType).join(',')}`, + required: 'required|boolean', + }; + const validation = Validator.init(body, rules); + if (validation.fails()) { + throw new ValidationException(validation); + } + + const groupExist = await VariantGroup.exists({ where: { name: body.name, restaurant_id: rest.id } }); + if (groupExist) { + throw new BadRequestException('Variant Group has already existed.'); + } + + const group = new VariantGroup(); + group.name = body.name; + group.type = body.type; + group.required = isTrue(body.required); + group.restaurant_id = rest.id; + await group.save(); + + return response.item(group, VariantGroupTransformer); + } + + @Put('/:group_id') + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Variant}@${PermAct.U}`) + async update(@Rest() rest, @Body() body, @Res() response, @Param() param) { + const rules = { + name: 'required|unique|safe_text', + type: `required|in:${Object.values(VariantGroupType).join(',')}`, + required: 'required|boolean', + }; + const validation = Validator.init(body, rules); + if (validation.fails()) { + throw new ValidationException(validation); + } + + if (!param.group_id) { + throw new BadRequestException(); + } + + const groupExist = await VariantGroup.exists({ + where: { name: body.name, restaurant_id: rest.id, id: Not(param.group_id) }, + }); + if (groupExist) { + throw new BadRequestException('Variant Group has already existed.'); + } + + const group = await VariantGroup.findOneByOrFail({ id: param.group_id }); + group.name = body.name; + group.type = body.type; + group.required = isTrue(body.required); + await group.save(); + + return response.item(group, VariantGroupTransformer); + } +} diff --git a/src/app/staff/restaurant/variant/variant.controller.ts b/src/app/staff/restaurant/variant/variant.controller.ts new file mode 100644 index 0000000..ffb32cc --- /dev/null +++ b/src/app/staff/restaurant/variant/variant.controller.ts @@ -0,0 +1,108 @@ +import { Rest } from '@core/decorators/restaurant.decorator'; +import { StaffAuthGuard } from '@core/guards/auth.guard'; +import { StaffGuard } from '@core/guards/staff.guard'; +import { PermAct, PermStaff } from '@core/services/role.service'; +import { VariantGroup } from '@db/entities/owner/variant-group.entity'; +import { Variant, VariantStatus } from '@db/entities/owner/variant.entity'; +import { VariantTransformer } from '@db/transformers/variant.transformer'; +import { ValidationException } from '@lib/exceptions/validation.exception'; +import { Validator } from '@lib/helpers/validator.helper'; +import { Permissions } from '@lib/rbac'; +import AppDataSource from '@lib/typeorm/datasource.typeorm'; +import { BadRequestException, Body, Controller, Get, Param, Post, Put, Res, UseGuards } from '@nestjs/common'; +import { Not } from 'typeorm'; + +@Controller() +@UseGuards(StaffAuthGuard()) +export class VariantController { + @Get() + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Variant}@${PermAct.R}`) + async index(@Rest() rest, @Res() response) { + const variants = await AppDataSource.createQueryBuilder(Variant, 't1') + .where({ restaurant_id: rest.id }) + .search() + .sort() + .getPaged(); + await response.paginate(variants, VariantTransformer); + } + + @Get('/:variant_id') + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Variant}@${PermAct.R}`) + async show(@Rest() rest, @Res() response, @Param() param) { + const variant = await Variant.findOneByOrFail({ restaurant_id: rest.id, id: param.variant_id }); + await response.item(variant, VariantTransformer); + } + + @Post() + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Variant}@${PermAct.C}`) + async create(@Rest() rest, @Body() body, @Res() response) { + const rules = { + name: 'required|unique|safe_text', + price: 'required|numeric|min:0', + status: `required|in:${Object.values(VariantStatus).join(',')}`, + group_id: 'required|uid', + }; + const validation = Validator.init(body, rules); + if (validation.fails()) { + throw new ValidationException(validation); + } + + const variantExist = await Variant.exists({ where: { name: body.name, restaurant_id: rest.id } }); + if (variantExist) { + throw new BadRequestException('Variant has already existed.'); + } + + const group = await VariantGroup.findOneByOrFail({ id: body.group_id }); + + const variant = new Variant(); + variant.name = body.name; + variant.price = body.price; + variant.status = body.status; + variant.restaurant_id = rest.id; + variant.group_id = group.id; + await variant.save(); + + return response.item(variant, VariantTransformer); + } + + @Put('/:variant_id') + @UseGuards(StaffGuard) + @Permissions(`${PermStaff.Variant}@${PermAct.U}`) + async update(@Rest() rest, @Body() body, @Res() response, @Param() param) { + const rules = { + name: 'required|unique|safe_text', + price: 'required|numeric|min:0', + status: `required|in:${Object.values(VariantStatus).join(',')}`, + group_id: 'required|uid', + }; + const validation = Validator.init(body, rules); + if (validation.fails()) { + throw new ValidationException(validation); + } + + if (!param.variant_id) { + throw new BadRequestException(); + } + + const variantExist = await Variant.exists({ + where: { name: body.name, restaurant_id: rest.id, id: Not(param.variant_id) }, + }); + if (variantExist) { + throw new BadRequestException('Variant has already existed.'); + } + + const group = await VariantGroup.findOneByOrFail({ id: body.group_id }); + + const variant = await Variant.findOneByOrFail({ id: param.variant_id }); + variant.name = body.name; + variant.price = body.price; + variant.status = body.status; + variant.group_id = group.id; + await variant.save(); + + return response.item(variant, VariantTransformer); + } +} diff --git a/src/app/staff/restaurant/variant/variant.module.ts b/src/app/staff/restaurant/variant/variant.module.ts new file mode 100644 index 0000000..56f0098 --- /dev/null +++ b/src/app/staff/restaurant/variant/variant.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { VariantGroupController } from './group.controller'; +import { VariantController } from './variant.controller'; + +@Module({ + imports: [], + controllers: [VariantController, VariantGroupController], + providers: [], +}) +export class StaffVariantModule {} diff --git a/src/app/staff/staff.module.ts b/src/app/staff/staff.module.ts new file mode 100644 index 0000000..127a2cb --- /dev/null +++ b/src/app/staff/staff.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import { StaffAuthModule } from './auth/auth.module'; +import { StaffProfileModule } from './profile/profile.module'; +import { StaffCategoryModule } from './restaurant/category/category.module'; +import { StaffProductModule } from './restaurant/product/product.module'; +import { StaffRestaurantModule } from './restaurant/restaurant.module'; +import { StaffStockModule } from './restaurant/stock/stock.module'; +import { StaffTableModule } from './restaurant/table/table.module'; +import { StaffVariantModule } from './restaurant/variant/variant.module'; + +@Module({ + imports: [ + StaffAuthModule, + StaffProfileModule, + StaffRestaurantModule, + StaffStockModule, + StaffTableModule, + StaffCategoryModule, + StaffVariantModule, + StaffProductModule, + ], + controllers: [], + providers: [], +}) +export class StaffModule {} diff --git a/src/config/database.config.ts b/src/config/database.config.ts index 5237533..349db69 100644 --- a/src/config/database.config.ts +++ b/src/config/database.config.ts @@ -71,6 +71,7 @@ const development: TypeOrmModuleOptions = { username: config.get('DATABASE_USER'), password: config.get('DATABASE_PASSWORD'), database: config.get('DATABASE_NAME'), + synchronize: true, }; export const database: TypeOrmModuleOptions = config.isDevelopment() ? development : production; diff --git a/src/core/core.module.ts b/src/core/core.module.ts index edd2821..3ab957a 100644 --- a/src/core/core.module.ts +++ b/src/core/core.module.ts @@ -10,9 +10,23 @@ import { DataSource } from 'typeorm'; import { AuthService } from './services/auth.service'; import { AwsService } from './services/aws.service'; import { CustomerService } from './services/customer.service'; +import { PdfService } from './services/pdf.service'; +import { ProductService } from './services/product.service'; import { RoleService } from './services/role.service'; import { TaskService } from './services/task.service'; -import { JwtOwnerStrategy, JwtStrategy } from './services/token.service'; +import { JwtOwnerStrategy, JwtStaffStrategy, JwtStrategy } from './services/token.service'; +import { UtilService } from './services/util.service'; + +const services = [ + AwsService, + TaskService, + RoleService, + AuthService, + CustomerService, + ProductService, + PdfService, + UtilService, +]; @Global() @Module({ @@ -24,13 +38,10 @@ import { JwtOwnerStrategy, JwtStrategy } from './services/token.service'; MulterExtendedModule.register(storage.aws), ], providers: [ - AwsService, - TaskService, + ...services, JwtStrategy, - AuthService, - RoleService, - CustomerService, JwtOwnerStrategy, + JwtStaffStrategy, { provide: DataSource, useFactory: async () => { @@ -38,17 +49,6 @@ import { JwtOwnerStrategy, JwtStrategy } from './services/token.service'; }, }, ], - exports: [ - AwsService, - TaskService, - RoleService, - AuthService, - CustomerService, - JwtStrategy, - JwtOwnerStrategy, - DataSource, - RBAcModule, - MulterExtendedModule, - ], + exports: [...services, JwtStrategy, JwtOwnerStrategy, JwtStaffStrategy, DataSource, RBAcModule, MulterExtendedModule], }) export class CoreModule {} diff --git a/src/core/guards/auth.guard.ts b/src/core/guards/auth.guard.ts index ef7167e..d4b8ac2 100644 --- a/src/core/guards/auth.guard.ts +++ b/src/core/guards/auth.guard.ts @@ -97,16 +97,6 @@ function createAuthGuard(type: string | string[] = 'jwt'): Type { export const AuthGuard: (type?: string | string[]) => Type = memoize(createAuthGuard); -function createOwnerAuthGuard(type: string | string[] = 'jwt-owner'): Type { - class MixinAuthGuard extends BaseAuthGuard implements CanActivate { - protected type: string | string[] = type; - } - - return mixin(MixinAuthGuard); -} - -export const OwnerAuthGuard: (type?: string | string[]) => Type = memoize(createOwnerAuthGuard); - function createBasicAuthGuard(type: string | string[] = 'jwt'): Type { class MixinAuthGuard extends BaseAuthGuard implements CanActivate { protected type: string | string[] = type; @@ -135,3 +125,25 @@ function createApiGuard(type: string | string[] = 'bearer'): Type { } export const ApiGuard: (type?: string | string[]) => Type = memoize(createApiGuard); + +// Owner Guard +function createOwnerAuthGuard(type: string | string[] = 'jwt-owner'): Type { + class MixinAuthGuard extends BaseAuthGuard implements CanActivate { + protected type: string | string[] = type; + } + + return mixin(MixinAuthGuard); +} + +export const OwnerAuthGuard: (type?: string | string[]) => Type = memoize(createOwnerAuthGuard); + +// Staff Guard +function createStaffAuthGuard(type: string | string[] = 'jwt-staff'): Type { + class MixinAuthGuard extends BaseAuthGuard implements CanActivate { + protected type: string | string[] = type; + } + + return mixin(MixinAuthGuard); +} + +export const StaffAuthGuard: (type?: string | string[]) => Type = memoize(createStaffAuthGuard); diff --git a/src/core/guards/staff.guard.ts b/src/core/guards/staff.guard.ts new file mode 100644 index 0000000..d4331f6 --- /dev/null +++ b/src/core/guards/staff.guard.ts @@ -0,0 +1,90 @@ +import { Location } from '@db/entities/owner/location.entity'; +import { Restaurant } from '@db/entities/owner/restaurant.entity'; +import { StaffRole } from '@db/entities/staff/role.entity'; +import { StaffUser } from '@db/entities/staff/user.entity'; +import { GuardException } from '@lib/exceptions/guard.exception'; +import { TokenException } from '@lib/exceptions/token.exception'; +import { ParamsFilter, RBAC_REQUEST_FILTER, RbacService } from '@lib/rbac'; +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { get } from 'lodash'; + +const validateRestaurant = async (request: any) => { + console.log(request); + const user: StaffUser = request.user; + if (!user) { + throw new TokenException('Getting user was failed'); + } + + const restaurantId = + get(request, 'params.restaurant_id') || get(request, 'headers.x-restaurant-id') || get(user, 'restaurant_id'); + if (!restaurantId) { + throw new GuardException('Restaurant is not found'); + } + + const restaurant = await Restaurant.findOneBy({ id: restaurantId }); + if (!restaurant) { + throw new TokenException('Getting restaurant was failed'); + } + + const roles = await StaffRole.findBy({ slug: user.role_slug }); + const role = roles.find((row) => row.slug !== 'owner'); + if (!role) { + throw new GuardException('Getting role was failed'); + } + + let location: Location = null; + if (user.location_id) { + location = await Location.findOneBy({ id: user.location_id }); + } + + return { user, restaurant, roles, role, location }; +}; + +export const setupPermission = async (request) => { + const { restaurant, role, roles, location } = await validateRestaurant(request); + + // assign additional data to request object + Object.assign(request, { + current: { + restaurant: restaurant, + role, + location, + }, + }); + + // filter based on available modules on plan + const grants = roles.reduce((acc, cur) => { + acc[cur.slug] = cur.permissions || []; + return acc; + }, {}); + + // assign grants to request helper + (request as any).requestContext.set('grants', grants); + + const filter = new ParamsFilter(); + filter.setParam(RBAC_REQUEST_FILTER, { ...request }); + + return { restaurant, role, roles, location, filter }; +}; + +@Injectable() +export class StaffGuard implements CanActivate { + constructor(private readonly reflector: Reflector, private readonly rbacService: RbacService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + + const permissions = this.reflector.get('Permissions', context.getHandler()); + if (!permissions) { + throw new GuardException('Bad permission'); + } + + const { role, filter } = await setupPermission(request); + if (!(await this.rbacService.getRole(role.slug, filter)).can(...permissions)) { + throw new GuardException('Forbidden resource'); + } + + return true; + } +} diff --git a/src/core/services/jwt.service.ts b/src/core/services/jwt.service.ts deleted file mode 100644 index bd02fc6..0000000 --- a/src/core/services/jwt.service.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { jwt } from '@config/jwt.config'; -import { Owner } from '@db/entities/owner/owner.entity'; -import { TokenException } from '@lib/exceptions/token.exception'; -import { Injectable } from '@nestjs/common'; -import { PassportStrategy } from '@nestjs/passport'; -import { ExtractJwt, Strategy, VerifiedCallback } from 'passport-jwt'; - -@Injectable() -export class JwtOwnerStrategy extends PassportStrategy(Strategy, 'jwt-owner') { - constructor() { - super({ - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - ignoreExpiration: false, - secretOrKey: jwt.secret, - }); - } - - // exposing payload - async validate(payload: any, done: VerifiedCallback) { - const user = await Owner.findOne(payload.sub); - if (!user) { - return done(new TokenException('Invalid token supplied'), false); - } - - return done(null, user, payload.iat); - } -} diff --git a/src/core/services/pdf.service.ts b/src/core/services/pdf.service.ts new file mode 100644 index 0000000..c492962 --- /dev/null +++ b/src/core/services/pdf.service.ts @@ -0,0 +1,52 @@ +import { handlebars } from '@lib/handlebars/adapter.library'; +import { config } from '@lib/helpers/config.helper'; +import { Injectable } from '@nestjs/common'; +import HTMLToPDF, { LaunchOptions, PDFOptions } from 'convert-html-to-pdf'; +import * as fs from 'fs'; + +export interface IPdfOption { + browser?: LaunchOptions; + pdf?: PDFOptions; + waitForNetworkIdle?: boolean; +} + +@Injectable() +export class PdfService { + create(template: string, data: any, options?: IPdfOption) { + const html = fs.readFileSync(`${config.getTemplatePath()}/pdf/${template}.hbs`, 'utf8'); + const compiled = handlebars.compile(html)(data); + + const htmlToPdf = new HTMLToPDF(compiled, { + waitForNetworkIdle: options.waitForNetworkIdle || false, + browserOptions: { + args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'], + ...(options.browser || {}), + }, + pdfOptions: { + printBackground: true, + ...(options.pdf || {}), + }, + }); + + return htmlToPdf.convert(); + } + + async tableLabel(data: any) { + return this.create( + 'label-table', + { data }, + { + pdf: { + width: '14.85cm', + height: '21cm', + margin: { + top: 0.5, + bottom: 0.5, + left: 0.5, + right: 0.5, + }, + }, + } + ); + } +} diff --git a/src/core/services/product.service.ts b/src/core/services/product.service.ts new file mode 100644 index 0000000..ed5f5ca --- /dev/null +++ b/src/core/services/product.service.ts @@ -0,0 +1,179 @@ +import { Category } from '@db/entities/owner/category.entity'; +import { ProductCategory } from '@db/entities/owner/product-category.entity'; +import { ProductVariant } from '@db/entities/owner/product-variant.entity'; +import { Product } from '@db/entities/owner/product.entity'; +import { Restaurant } from '@db/entities/owner/restaurant.entity'; +import { VariantGroup } from '@db/entities/owner/variant-group.entity'; +import { Variant, VariantStatus } from '@db/entities/owner/variant.entity'; +import AppDataSource from '@lib/typeorm/datasource.typeorm'; +import { BadRequestException, Injectable } from '@nestjs/common'; +import { In } from 'typeorm'; + +@Injectable() +export class ProductService { + async create(rest: Restaurant, body: any): Promise { + try { + const productExist = await Product.exists({ where: { sku: body.sku, restaurant_id: rest.id } }); + if (productExist) { + throw new BadRequestException('Product SKU has already existed.'); + } + + const prod = new Product(); + prod.sku = body.sku; + prod.name = body.name; + prod.description = body.description; + prod.price = body.price; + prod.status = body.status; + prod.restaurant_id = rest.id; + + let categories: Category[] = []; + let variants: Variant[] = []; + + if (body.category_ids.length > 0) { + categories = await Category.find({ where: { id: In(body.category_ids), restaurant_id: rest.id } }); + } + + if (body.variant_ids.length > 0) { + variants = await Variant.find({ where: { id: In(body.variant_ids), restaurant_id: rest.id } }); + } + + await AppDataSource.transaction(async (manager) => { + // Save Product + await manager.getRepository(Product).save(prod); + + // Save Category + if (categories.length > 0) { + const pcats: ProductCategory[] = []; + for (const category of categories) { + const pcat = new ProductCategory(); + pcat.product_id = prod.id; + pcat.category_id = category.id; + pcats.push(pcat); + } + + await manager.getRepository(ProductCategory).save(pcats); + } + + const pvars: ProductVariant[] = []; + + // Create default variant for single product + const pvar = new ProductVariant(); + pvar.product_id = prod.id; + pvar.price = prod.price; + pvar.status = VariantStatus.Available; + pvar.restaurant_id = rest.id; + pvars.push(pvar); + + // Save Variants + if (variants.length > 0) { + for (const variant of variants) { + const vari = new ProductVariant(); + vari.status = variant.status; + vari.product_id = prod.id; + vari.variant_id = variant.id; + vari.price = variant.price; + vari.restaurant_id = rest.id; + pvars.push(vari); + } + } + + await manager.getRepository(ProductVariant).save(pvars); + + // @TODO: Create Product History + }); + + return prod; + } catch (error) { + throw error; + } + } + + async assignCategories(rest: Restaurant, id: string, category_ids: string[]): Promise { + try { + const product = await Product.findOrFail({ where: { id, restaurant_id: rest.id } }); + const prodCategories: ProductCategory[] = []; + + const categories = await Category.findBy({ id: In(category_ids) }); + for (const category of categories) { + const isExist = await ProductCategory.exists({ where: { product_id: product.id, category_id: category.id } }); + if (isExist) { + continue; + } + + const pcat = new ProductCategory(); + pcat.product_id = product.id; + pcat.category_id = category.id; + prodCategories.push(pcat); + } + + if (prodCategories.length > 0) { + await ProductCategory.save(prodCategories); + } + + return product; + } catch (error) { + throw error; + } + } + + async assignVariant(rest: Restaurant, id: string, variant_ids: string[]): Promise { + try { + const product = await Product.findOrFail({ where: { id, restaurant_id: rest.id } }); + const prodVariants: ProductVariant[] = []; + + const variants = await Variant.findBy({ id: In(variant_ids) }); + for (const variant of variants) { + const isExist = await ProductVariant.exists({ where: { product_id: product.id, variant_id: variant.id } }); + if (isExist) { + continue; + } + + const pcat = new ProductVariant(); + pcat.product_id = product.id; + pcat.variant_id = variant.id; + pcat.status = variant.status; + pcat.price = variant.price; + pcat.restaurant_id = rest.id; + prodVariants.push(pcat); + } + + if (prodVariants.length > 0) { + await ProductVariant.save(prodVariants); + } + return product; + } catch (error) { + throw error; + } + } + + async getVariants(id: string, rest: Restaurant): Promise { + const product = await Product.findOrFail({ where: { id, restaurant_id: rest.id } }); + const productVariants = await ProductVariant.findBy({ product_id: product.id }); + const variants = await Variant.find({ + where: { id: In(productVariants.map((val) => val.variant_id)) }, + order: { price: 'ASC' }, + }); + const groups = await VariantGroup.find({ + where: { id: In(variants.map((val) => val.group_id)) }, + order: { name: 'ASC' }, + }); + + const result: any[] = []; + + for (const group of groups) { + const payload = { + ...group, + variants: [], + }; + + for (const variant of variants) { + if (variant.group_id === group.id) { + payload.variants.push(variant); + } + } + + result.push(payload); + } + return result; + } +} diff --git a/src/core/services/role.service.ts b/src/core/services/role.service.ts index 405ecc2..e23f7da 100644 --- a/src/core/services/role.service.ts +++ b/src/core/services/role.service.ts @@ -11,19 +11,35 @@ export enum PermAct { } export enum PermOwner { - Profile = 'profile', - Restaurant = 'restaurant', - Staff = 'staff', - Role = 'role', - Location = 'location', - Table = 'table', - Category = 'category', - Variant = 'variant', - Product = 'product', - Stock = 'stock', + Profile = 'owner_profile', + Restaurant = 'owner_restaurant', + Staff = 'owner_staff', + Role = 'owner_role', + Location = 'owner_location', + Table = 'owner_table', + Category = 'owner_category', + Variant = 'owner_variant', + Product = 'owner_product', + Stock = 'owner_stock', + Order = 'owner_order', + Notification = 'owner_notification', } -export const DefaultPerms = [PermOwner.Profile, PermOwner.Restaurant]; +export enum PermStaff { + Profile = 'staff_profile', + Restaurant = 'staff_restaurant', + Role = 'staff_role', + Location = 'staff_location', + Table = 'staff_table', + Category = 'staff_category', + Variant = 'staff_variant', + Product = 'staff_product', + Stock = 'staff_stock', + Order = 'staff_order', + Notification = 'staff_notification', +} + +export const DefaultPerms = [PermOwner.Profile, PermOwner.Restaurant, PermStaff.Profile]; @Injectable() export class RoleService implements IDynamicStorageRbac { @@ -45,6 +61,21 @@ export class RoleService implements IDynamicStorageRbac { [PermOwner.Variant]: [PermAct.R, PermAct.C, PermAct.U, PermAct.D], [PermOwner.Product]: [PermAct.R, PermAct.C, PermAct.U, PermAct.D], [PermOwner.Stock]: [PermAct.R, PermAct.C, PermAct.U, PermAct.D], + [PermOwner.Order]: [PermAct.R, PermAct.C, PermAct.U, PermAct.D], + [PermOwner.Notification]: [PermAct.R, PermAct.C, PermAct.U, PermAct.D], + + [PermStaff.Profile]: [PermAct.R, PermAct.C, PermAct.U, PermAct.D], + [PermStaff.Restaurant]: [PermAct.R, PermAct.C, PermAct.U, PermAct.D], + // [PermStaff.Staff]: [PermAct.R, PermAct.C, PermAct.U, PermAct.D], + [PermStaff.Role]: [PermAct.R, PermAct.C, PermAct.U, PermAct.D], + [PermStaff.Location]: [PermAct.R, PermAct.C, PermAct.U, PermAct.D], + [PermStaff.Table]: [PermAct.R, PermAct.C, PermAct.U, PermAct.D], + [PermStaff.Category]: [PermAct.R, PermAct.C, PermAct.U, PermAct.D], + [PermStaff.Variant]: [PermAct.R, PermAct.C, PermAct.U, PermAct.D], + [PermStaff.Product]: [PermAct.R, PermAct.C, PermAct.U, PermAct.D], + [PermStaff.Stock]: [PermAct.R, PermAct.C, PermAct.U, PermAct.D], + [PermStaff.Order]: [PermAct.R, PermAct.C, PermAct.U, PermAct.D], + [PermStaff.Notification]: [PermAct.R, PermAct.C, PermAct.U, PermAct.D], }; return { diff --git a/src/core/services/token.service.ts b/src/core/services/token.service.ts index 958ece2..8b4f097 100644 --- a/src/core/services/token.service.ts +++ b/src/core/services/token.service.ts @@ -1,6 +1,7 @@ import { jwt } from '@config/jwt.config'; import { Customer } from '@db/entities/core/customer.entity'; import { Owner } from '@db/entities/owner/owner.entity'; +import { StaffUser } from '@db/entities/staff/user.entity'; import { TokenException } from '@lib/exceptions/token.exception'; import { Injectable } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; @@ -47,3 +48,24 @@ export class JwtOwnerStrategy extends PassportStrategy(Strategy, 'jwt-owner') { return done(null, user, payload.iat); } } + +@Injectable() +export class JwtStaffStrategy extends PassportStrategy(Strategy, 'jwt-staff') { + constructor() { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: jwt.secret, + }); + } + + // exposing payload + async validate(payload: any, done: VerifiedCallback) { + const user = await StaffUser.findOneBy({ id: payload.sub }); + if (!user) { + return done(new TokenException('Invalid token supplied'), false); + } + + return done(null, user, payload.iat); + } +} diff --git a/src/core/services/util.service.ts b/src/core/services/util.service.ts new file mode 100644 index 0000000..853132c --- /dev/null +++ b/src/core/services/util.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; +import * as bwip from 'bwip-js'; +import { ToBufferOptions } from 'bwip-js'; + +export type ICodePayload = Partial; + +@Injectable() +export class UtilService { + getBarCode(text: string, options?: ICodePayload): any { + return bwip.toBuffer({ + text, + bcid: 'code128', + height: 10, + ...(options || {}), + }); + } + + getQrCode(text: string, options: ICodePayload): any { + return bwip.toBuffer({ + text, + bcid: 'qrcode', + scale: 1, + ...(options || {}), + }); + } +} diff --git a/src/database/entities/base/basic.ts b/src/database/entities/base/basic.ts index 91171ad..50f072b 100644 --- a/src/database/entities/base/basic.ts +++ b/src/database/entities/base/basic.ts @@ -9,7 +9,7 @@ import { DeepPartial, FindManyOptions, FindOneOptions, - ObjectID, + ObjectId, RemoveOptions, SaveOptions, } from 'typeorm'; @@ -51,7 +51,7 @@ export class BasicEntity extends Base { this: { new (): T; } & typeof Base, - id?: string | number | Date | ObjectID, + id?: string | number | Date | ObjectId, restaurant?: any ): Promise { const obj = await this.findOne({ where: { id, restaurant_id: restaurant.id } } as any); diff --git a/src/database/entities/core/notification.entity.ts b/src/database/entities/core/notification.entity.ts new file mode 100644 index 0000000..01d906f --- /dev/null +++ b/src/database/entities/core/notification.entity.ts @@ -0,0 +1,49 @@ +import { BooleanColumn, Column, CoreEntity, ForeignColumn } from '@lib/typeorm/decorators'; +import { ManyToOne } from 'typeorm'; +import { BaseEntity } from '../base/base'; +import { Location } from '../owner/location.entity'; +import { Restaurant } from '../owner/restaurant.entity'; +import { Order } from './order.entity'; + +export enum NotificationType { + OrderCreated = 'order_created', + OrderUpdate = 'order_update', +} + +@CoreEntity() +export class Notification extends BaseEntity { + public static sortable = ['created_at']; + + @Column({ length: 100 }) + type: NotificationType; + + @Column() + title: string; + + @Column({ type: 'text' }) + content: string; + + @Column({ length: 100 }) + actor: string; + + @BooleanColumn({ default: 0 }) + is_read: boolean; + + @ForeignColumn() + location_id: string; + + @ManyToOne(() => Location, { onDelete: 'CASCADE' }) + location: Location; + + @ForeignColumn() + restaurant_id: string; + + @ManyToOne(() => Restaurant, { onDelete: 'CASCADE' }) + restaurant: Restaurant; + + @ForeignColumn() + order_id: string; + + @ManyToOne(() => Order, { onDelete: 'CASCADE' }) + order: Order; +} diff --git a/src/database/entities/core/order-product.entity.ts b/src/database/entities/core/order-product.entity.ts index f92b4f6..c749fcb 100644 --- a/src/database/entities/core/order-product.entity.ts +++ b/src/database/entities/core/order-product.entity.ts @@ -26,6 +26,7 @@ export class OrderProduct extends BaseEntity { @ForeignColumn() product_variant_id: string; + @Exclude() @JoinColumn() @ManyToOne(() => ProductVariant, (pv) => pv.order_products, { onDelete: 'CASCADE' }) product_variant: Promise; diff --git a/src/database/entities/core/order.entity.ts b/src/database/entities/core/order.entity.ts index 35f7586..a4286b6 100644 --- a/src/database/entities/core/order.entity.ts +++ b/src/database/entities/core/order.entity.ts @@ -8,7 +8,7 @@ import { StatusColumn, } from '@lib/typeorm/decorators'; import { Exclude } from 'class-transformer'; -import { Column, Generated, JoinColumn, ManyToOne, OneToMany } from 'typeorm'; +import { Brackets, Column, Generated, Index, JoinColumn, ManyToOne, OneToMany, SelectQueryBuilder } from 'typeorm'; import { Location } from '../owner/location.entity'; import { Restaurant } from '../owner/restaurant.entity'; import { Table } from '../owner/table.entity'; @@ -20,28 +20,33 @@ export enum OrderStatus { Confirmed = 'confirmed', Preparing = 'preparing', Served = 'served', + WaitingPayment = 'waiting_payment', Completed = 'completed', Cancelled = 'cancelled', } @CoreEntity({ autoIncrement: 202410000001 }) export class Order extends BaseEntity { + public static sortable = ['number', 'status', 'created_at']; + public static searchable = ['search']; + @Exclude() @Generated('increment') @NotNullColumn({ type: 'bigint', unique: true }) uid: number; + @Index() @Column({ length: 50 }) number: string; @PriceColumn() - gross_total: string; + gross_total: number; @PriceColumn() - discount: string; + discount: number; @PriceColumn() - net_total: string; + net_total: number; @DateTimeColumn() billed_at: Date; @@ -52,6 +57,12 @@ export class Order extends BaseEntity { @Column({ nullable: true }) note: string; + @Column() + customer_name: string; + + @Column({ nullable: true }) + customer_phone: string; + @Exclude() @ForeignColumn() customer_id: string; @@ -87,4 +98,12 @@ export class Order extends BaseEntity { @Exclude() @OneToMany(() => OrderProduct, (op) => op.order) order_products: Promise; + + static onFilterSearch(value: string, builder: SelectQueryBuilder) { + builder.nextWhere( + new Brackets((qb) => { + qb.where('t1.number LIKE :query', { query: `%${value}%` }); + }) + ); + } } diff --git a/src/database/entities/owner/product-stock.entity.ts b/src/database/entities/owner/product-stock.entity.ts index 6491018..aaac6a3 100644 --- a/src/database/entities/owner/product-stock.entity.ts +++ b/src/database/entities/owner/product-stock.entity.ts @@ -30,7 +30,6 @@ export class ProductStock extends BaseEntity { @Column({ length: 100, nullable: true }) actor: string; - @Exclude() @ForeignColumn() variant_id: string; @@ -46,7 +45,6 @@ export class ProductStock extends BaseEntity { @ManyToOne(() => Product, (variant) => variant.stocks) product: Promise; - @Exclude() @ForeignColumn() location_id: string; diff --git a/src/database/entities/owner/restaurant.entity.ts b/src/database/entities/owner/restaurant.entity.ts index 00491f3..ec41ac9 100644 --- a/src/database/entities/owner/restaurant.entity.ts +++ b/src/database/entities/owner/restaurant.entity.ts @@ -26,7 +26,7 @@ export class Restaurant extends BaseEntity { @PhoneColumn() phone: string; - @Column({ type: 'longtext' }) + @Column({ type: 'longtext', nullable: true }) description: string; @Column() diff --git a/src/database/entities/owner/table.entity.ts b/src/database/entities/owner/table.entity.ts index f8ad372..5c962d2 100644 --- a/src/database/entities/owner/table.entity.ts +++ b/src/database/entities/owner/table.entity.ts @@ -11,7 +11,6 @@ export enum TableStatus { InUse = 'in_use', Reserved = 'reserved', Unvailable = 'unvailable', - Empty = 'empty', } @CoreEntity() @@ -28,7 +27,7 @@ export class Table extends BaseEntity { @JoinColumn() @ManyToOne(() => Restaurant, (restaurant) => restaurant.tables, { onDelete: 'CASCADE' }) - restaurant: Restaurant; + restaurant: Promise; @Exclude() @ForeignColumn() diff --git a/src/database/entities/staff/user.entity.ts b/src/database/entities/staff/user.entity.ts index 0fe8d60..72c8a80 100644 --- a/src/database/entities/staff/user.entity.ts +++ b/src/database/entities/staff/user.entity.ts @@ -57,7 +57,7 @@ export class StaffUser extends BaseEntity { last_login_at: Date; @OneToOne(() => Media, (media) => media.staff_user, { eager: true }) - media: Media; + image: Media; @Exclude() @ForeignColumn() @@ -85,6 +85,10 @@ export class StaffUser extends BaseEntity { return this.status === StaffUserStatus.Blocked; } + get isActive() { + return [StaffUserStatus.Active].includes(this.status); + } + get logName() { return `${this.name} <${this.email}>`; } diff --git a/src/database/interfaces/order.interface.ts b/src/database/interfaces/order.interface.ts index 9aea965..6c1b6e6 100644 --- a/src/database/interfaces/order.interface.ts +++ b/src/database/interfaces/order.interface.ts @@ -13,7 +13,7 @@ export interface IOrderItems { export interface IOrderDetail { restaurant_id: string; table_id: string; - location_id: string; + location_id?: string; reference?: string; customer_name: string; customer_phone?: string; diff --git a/src/database/migrations/1718551497693-order-number-index.ts b/src/database/migrations/1718551497693-order-number-index.ts new file mode 100644 index 0000000..4cfbac0 --- /dev/null +++ b/src/database/migrations/1718551497693-order-number-index.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class OrderNumberIndex1718551497693 implements MigrationInterface { + name = 'OrderNumberIndex1718551497693'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE INDEX \`IDX_2bf21e468cc540c1ac7645da26\` ON \`order\` (\`number\`)`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX \`IDX_2bf21e468cc540c1ac7645da26\` ON \`order\``); + } +} diff --git a/src/database/migrations/1719250498189-notification.ts b/src/database/migrations/1719250498189-notification.ts new file mode 100644 index 0000000..f4eb84d --- /dev/null +++ b/src/database/migrations/1719250498189-notification.ts @@ -0,0 +1,27 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class Notification1719250498189 implements MigrationInterface { + name = 'Notification1719250498189'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE \`notification\` (\`id\` varchar(26) NOT NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), \`type\` varchar(100) NULL, \`title\` varchar(255) NULL, \`content\` text NULL, \`actor\` varchar(100) NULL, \`location_id\` varchar(26) NULL, \`restaurant_id\` varchar(26) NULL, \`order_id\` varchar(26) NULL, PRIMARY KEY (\`id\`)) ENGINE=InnoDB` + ); + await queryRunner.query( + `ALTER TABLE \`notification\` ADD CONSTRAINT \`FK_9f0f4d05e60d2d94135eda86649\` FOREIGN KEY (\`location_id\`) REFERENCES \`location\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE \`notification\` ADD CONSTRAINT \`FK_eea2206a8461ccbcfc11f1c45a5\` FOREIGN KEY (\`restaurant_id\`) REFERENCES \`restaurant\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE \`notification\` ADD CONSTRAINT \`FK_3ea5cd8a1de9cbf90c86dd0582c\` FOREIGN KEY (\`order_id\`) REFERENCES \`order\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`notification\` DROP FOREIGN KEY \`FK_3ea5cd8a1de9cbf90c86dd0582c\``); + await queryRunner.query(`ALTER TABLE \`notification\` DROP FOREIGN KEY \`FK_eea2206a8461ccbcfc11f1c45a5\``); + await queryRunner.query(`ALTER TABLE \`notification\` DROP FOREIGN KEY \`FK_9f0f4d05e60d2d94135eda86649\``); + await queryRunner.query(`DROP TABLE \`notification\``); + } +} diff --git a/src/database/migrations/1719251773021-order_customer.ts b/src/database/migrations/1719251773021-order_customer.ts new file mode 100644 index 0000000..2e9ed01 --- /dev/null +++ b/src/database/migrations/1719251773021-order_customer.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class OrderCustomer1719251773021 implements MigrationInterface { + name = 'OrderCustomer1719251773021'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`order\` ADD \`customer_name\` varchar(255) NOT NULL`); + await queryRunner.query(`ALTER TABLE \`order\` ADD \`customer_phone\` varchar(255) NULL`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`order\` DROP COLUMN \`customer_phone\``); + await queryRunner.query(`ALTER TABLE \`order\` DROP COLUMN \`customer_name\``); + } +} diff --git a/src/database/migrations/1719334195161-notification_read.ts b/src/database/migrations/1719334195161-notification_read.ts new file mode 100644 index 0000000..2e81336 --- /dev/null +++ b/src/database/migrations/1719334195161-notification_read.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class NotificationRead1719334195161 implements MigrationInterface { + name = 'NotificationRead1719334195161'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`notification\` ADD \`is_read\` tinyint NULL DEFAULT '0'`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`notification\` DROP COLUMN \`is_read\``); + } +} diff --git a/src/database/seeds/dump/initial.sql b/src/database/seeds/dump/initial.sql index 3e912de..b01aab1 100644 --- a/src/database/seeds/dump/initial.sql +++ b/src/database/seeds/dump/initial.sql @@ -1,23 +1,75 @@ - -SET FOREIGN_KEY_CHECKS=0; +SET + FOREIGN_KEY_CHECKS = 0; -- -- Dumping data for table `role` -- - -INSERT INTO `role` (`id`, `slug`, `permissions`, `created_at`) VALUES -('01EP4ZDWCZCHVFA1ZHV13YGRJZ', 'owner', '[\"restaurant@read\", \"restaurant@create\", \"restaurant@update\", \"restaurant@delete\"]', '2023-01-06 16:00:00.501148'); +INSERT INTO + `role` (`id`, `slug`, `permissions`, `created_at`) +VALUES + ( + '01EP4ZDWCZCHVFA1ZHV13YGRJZ', + 'owner', + '[\"role@read\",\"role@create\",\"role@update\",\"role@delete\",\"restaurant@read\",\"restaurant@create\",\"restaurant@update\",\"restaurant@delete\",\"profile@read\",\"profile@create\",\"profile@update\",\"profile@delete\",\"staff@read\",\"staff@create\",\"staff@update\",\"staff@delete\",\"location@read\",\"location@create\",\"location@update\",\"location@delete\",\"table@read\",\"table@create\",\"table@update\",\"table@delete\",\"category@read\",\"category@create\",\"category@update\",\"category@delete\",\"variant@read\",\"variant@create\",\"variant@update\",\"variant@delete\",\"product@read\",\"product@create\",\"product@update\",\"product@delete\",\"stock@read\",\"stock@create\",\"stock@update\",\"stock@delete\",\"order@read\",\"order@create\",\"order@update\",\"order@delete\"]', + '2023-01-06 16:00:00.501148' + ); -- -- Dumping data for table `owner` -- - -INSERT INTO `owner` (`id`, `name`, `email`, `phone`, `password`, `verification_code`, `status`, `verified_at`, `last_login_at`, `created_at`, `restaurant_id`) VALUES -('01F2KFTXZNS01CJQCGNPJKXA1N', 'Yudha', 'owner@yuppey.com', '+6281931006841', '$2a$10$9/R3pR5rP1gnnG0n06pbEORY39fXfuJ2.eJkdqvoi5oDScm1gcHRi', NULL, 'verify', NULL, NULL, '2023-01-06 16:00:00.501148', '01F2KFTXX9W2Y630FKYSSC9NSQ'); +INSERT INTO + `owner` ( + `id`, + `name`, + `email`, + `phone`, + `password`, + `verification_code`, + `status`, + `verified_at`, + `last_login_at`, + `created_at`, + `restaurant_id` + ) +VALUES + ( + '01F2KFTXZNS01CJQCGNPJKXA1N', + 'Yudha', + 'owner@yuppey.com', + '+6281931006841', + '$2a$10$9/R3pR5rP1gnnG0n06pbEORY39fXfuJ2.eJkdqvoi5oDScm1gcHRi', + NULL, + 'verify', + NULL, + NULL, + '2023-01-06 16:00:00.501148', + '01F2KFTXX9W2Y630FKYSSC9NSQ' + ); -- -- Dumping data for table `restaurant` -- - -INSERT INTO `restaurant` (`id`, `name`, `slug`, `email`, `phone`, `website`, `status`, `created_at`, `owner_id`) VALUES -('01F2KFTXX9W2Y630FKYSSC9NSQ', 'Yuppey', 'yuppey', NULL, NULL, NULL, 'active', '2023-01-06 16:00:00.501148', '01F2KFTXZNS01CJQCGNPJKXA1N'); +INSERT INTO + `restaurant` ( + `id`, + `name`, + `slug`, + `email`, + `phone`, + `website`, + `status`, + `created_at`, + `owner_id` + ) +VALUES + ( + '01F2KFTXX9W2Y630FKYSSC9NSQ', + 'Yuppey', + 'yuppey', + NULL, + NULL, + NULL, + 'active', + '2023-01-06 16:00:00.501148', + '01F2KFTXZNS01CJQCGNPJKXA1N' + ); \ No newline at end of file diff --git a/src/database/subscribers/order.controller.ts b/src/database/subscribers/order.controller.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/database/transformers/notification.transformer.ts b/src/database/transformers/notification.transformer.ts new file mode 100644 index 0000000..40bd672 --- /dev/null +++ b/src/database/transformers/notification.transformer.ts @@ -0,0 +1,8 @@ +import { Notification } from '@db/entities/core/notification.entity'; +import { TransformerAbstract } from '@lib/transformer/abstract.transformer'; + +export class NotificationTransformer extends TransformerAbstract { + transform(entity: Notification) { + return entity.toJSON(); + } +} diff --git a/src/database/transformers/order-product.transformer.ts b/src/database/transformers/order-product.transformer.ts new file mode 100644 index 0000000..dcd0c7b --- /dev/null +++ b/src/database/transformers/order-product.transformer.ts @@ -0,0 +1,21 @@ +import { OrderProduct } from '@db/entities/core/order-product.entity'; +import { TransformerAbstract } from '@lib/transformer/abstract.transformer'; +import { ProductVariantTransformer } from './product-variant.transformer'; + +export class OrderProductTransformer extends TransformerAbstract { + get availableInclude() { + return ['item']; + } + + async includeItem(entity: OrderProduct) { + const product = await entity.product_variant; + if (!product) { + return this.null(); + } + return this.item(product, ProductVariantTransformer); + } + + transform(entity: OrderProduct) { + return entity.toJSON(); + } +} diff --git a/src/database/transformers/order.tranformer.ts b/src/database/transformers/order.tranformer.ts deleted file mode 100644 index 6b4f083..0000000 --- a/src/database/transformers/order.tranformer.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Order } from '@db/entities/core/order.entity'; -import { ProductVariant } from '@db/entities/owner/product-variant.entity'; -import { TransformerAbstract } from '@lib/transformer/abstract.transformer'; -import { In } from 'typeorm'; -import { ProductVariantTransformer } from './product-variant.transformer'; - -export class OrderTransformer extends TransformerAbstract { - get availableInclude() { - return ['items']; - } - - async includeItems(entity: Order) { - const orderProducts = await entity.order_products; - if (!orderProducts) { - return this.null(); - } - - let items: ProductVariant[] = []; - if (orderProducts.filter((val) => val.product_variant_id).length > 0) { - items = await ProductVariant.findBy({ id: In(orderProducts.map((val) => val.product_variant_id)) }); - } - - return this.collection(items, ProductVariantTransformer); - } - - transform(entity: Order) { - return entity.toJSON(); - } -} diff --git a/src/database/transformers/order.transformer.ts b/src/database/transformers/order.transformer.ts new file mode 100644 index 0000000..8eb199d --- /dev/null +++ b/src/database/transformers/order.transformer.ts @@ -0,0 +1,71 @@ +import { Media } from '@db/entities/core/media.entity'; +import { OrderProduct, OrderProductStatus } from '@db/entities/core/order-product.entity'; +import { Order } from '@db/entities/core/order.entity'; +import { Product } from '@db/entities/owner/product.entity'; +import { Variant } from '@db/entities/owner/variant.entity'; +import { TransformerAbstract } from '@lib/transformer/abstract.transformer'; +import { RawTransformer } from './raw.transformer'; + +export class OrderTransformer extends TransformerAbstract { + get availableInclude() { + return ['items', 'table', 'stats', 'location']; + } + + async includeItems(entity: Order) { + const orderProducts = await entity.order_products; + if (!orderProducts) { + return this.null(); + } + + const items: { + id: string; + qty: number; + price: number; + status: OrderProductStatus; + product: Product; + images: Media[]; + variant: Variant; + }[] = []; + if (orderProducts.length > 0) { + for (const orderProduct of orderProducts) { + const productVar = await orderProduct.product_variant; + const product = await productVar.product; + const variant = await productVar.variant; + const images = await Media.findBy({ product_id: product.id }); + + items.push({ + id: orderProduct.id, + qty: orderProduct.qty, + price: orderProduct.price, + status: orderProduct.status, + product, + images, + variant, + }); + } + } + + return this.collection(items, RawTransformer); + } + + async includeTable(entity: Order) { + return await entity.table; + } + + async includeStats(entity: Order) { + const data = { total_items: 0, total_preparing_items: 0 }; + + data.total_items = await OrderProduct.countBy({ order_id: entity.id }); + data.total_preparing_items = await OrderProduct.countBy({ order_id: entity.id, status: OrderProductStatus.Preparing }); + + return this.item(data, RawTransformer); + } + + async includeLocation(entity: Order) { + return await entity.location; + } + + transform(entity: Order) { + return entity.toJSON(); + } +} diff --git a/src/database/transformers/product.transformer.ts b/src/database/transformers/product.transformer.ts index 8af713d..9a1583e 100644 --- a/src/database/transformers/product.transformer.ts +++ b/src/database/transformers/product.transformer.ts @@ -6,10 +6,11 @@ import { In } from 'typeorm'; import { CategoryTransformer } from './category.transformer'; import { MediaTransformer } from './media.transformer'; import { ProductVariantTransformer } from './product-variant.transformer'; +import { RawTransformer } from './raw.transformer'; export class ProductTransformer extends TransformerAbstract { get availableInclude() { - return ['variants', 'categories', 'images']; + return ['variants', 'categories', 'images', 'stocks']; } async includeVariants(entity: Product) { @@ -40,6 +41,15 @@ export class ProductTransformer extends TransformerAbstract { return this.collection(media, MediaTransformer); } + async includeStocks(entity: Product) { + const stocks = await entity.stocks; + if (!stocks) { + return this.null(); + } + + return this.collection(stocks, RawTransformer); + } + transform(entity: Product) { return entity.toJSON(); } diff --git a/src/database/transformers/staff.transformer.ts b/src/database/transformers/staff.transformer.ts index 1406fbf..1501145 100644 --- a/src/database/transformers/staff.transformer.ts +++ b/src/database/transformers/staff.transformer.ts @@ -1,13 +1,15 @@ import { Media } from '@db/entities/core/media.entity'; +import { StaffRole } from '@db/entities/staff/role.entity'; import { StaffUser } from '@db/entities/staff/user.entity'; import { encrypt } from '@lib/helpers/encrypt.helper'; import { RequestHelper } from '@lib/helpers/request.helper'; import { TransformerAbstract } from '@lib/transformer/abstract.transformer'; import { LocationTransformer } from './location.transformer'; +import { RestaurantTransformer } from './restaurant.transformer'; export class StaffTransformer extends TransformerAbstract { get availableInclude() { - return ['location', 'role']; + return ['restaurant', 'location', 'role']; } get defaultInclude() { @@ -16,17 +18,17 @@ export class StaffTransformer extends TransformerAbstract { transform(entity: StaffUser) { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { media, ...rest } = entity.toJSON(); + const { image, ...rest } = entity.toJSON(); return { ...rest, - avatar: Media.getImage(entity.media), + avatar: Media.getImage(entity.image), }; } async transformWithPermission(entity: StaffUser) { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { media, ...rest } = entity.toJSON(); + const { image, ...rest } = entity.toJSON(); const grants = RequestHelper.getPermissionGrants(); const role = await entity.role; @@ -44,10 +46,19 @@ export class StaffTransformer extends TransformerAbstract { name: location.name, } : null, - avatar: Media.getImage(entity.media), + avatar: Media.getImage(entity.image), }; } + async includeRestaurant(entity: StaffUser) { + const restaurant = await entity.restaurant; + if (!restaurant) { + return this.null(); + } + + return this.item(restaurant, RestaurantTransformer); + } + async includeLocation(entity: StaffUser) { const location = await entity.location; if (!location) { @@ -58,11 +69,12 @@ export class StaffTransformer extends TransformerAbstract { } async includeRole(entity: StaffUser) { - const role = await entity.role; + const role = await StaffRole.findOneBy({ slug: entity.role_slug }); if (!role) { return this.null(); } - return { id: role.id, name: role.slug }; + const grants = RequestHelper.getPermissionGrants(); + return { id: role.id, name: role.slug, permissions: encrypt(grants[role.slug]) }; } } diff --git a/src/database/transformers/table.transformer.ts b/src/database/transformers/table.transformer.ts index 2513ae1..1102ed2 100644 --- a/src/database/transformers/table.transformer.ts +++ b/src/database/transformers/table.transformer.ts @@ -1,10 +1,23 @@ +import { Order, OrderStatus } from '@db/entities/core/order.entity'; import { Table } from '@db/entities/owner/table.entity'; import { TransformerAbstract } from '@lib/transformer/abstract.transformer'; +import { In, Not } from 'typeorm'; import { LocationTransformer } from './location.transformer'; +import { OrderTransformer } from './order.transformer'; +import { RestaurantTransformer } from './restaurant.transformer'; export class TableTransformer extends TransformerAbstract { get availableInclude() { - return ['location']; + return ['restaurant', 'location', 'order']; + } + + async includeRestaurant(entity: Table) { + const restaurant = await entity.restaurant; + if (!restaurant) { + return this.null(); + } + + return this.item(restaurant, RestaurantTransformer); } async includeLocation(entity: Table) { @@ -16,6 +29,19 @@ export class TableTransformer extends TransformerAbstract { return this.item(location, LocationTransformer); } + async includeOrder(entity: Table) { + const order = await Order.findOneBy({ + table_id: entity.id, + status: Not(In([OrderStatus.Completed, OrderStatus.Cancelled])), + }); + + if (!order) { + return this.null(); + } + + return this.item(order, OrderTransformer); + } + transform(entity: Table) { return entity.toJSON(); } diff --git a/src/library/helpers/config.helper.ts b/src/library/helpers/config.helper.ts index 1ab4490..ca6f5e2 100755 --- a/src/library/helpers/config.helper.ts +++ b/src/library/helpers/config.helper.ts @@ -38,6 +38,10 @@ class ConfigService { return path.toString(); } + public getAppURI() { + return `${this.get('APP_URI')}`; + } + public getAssetURI() { return `${this.get('API_URI')}/assets`; } diff --git a/src/library/pubsub/pubsub.lib.ts b/src/library/pubsub/pubsub.lib.ts index 5fe8dec..db00c30 100644 --- a/src/library/pubsub/pubsub.lib.ts +++ b/src/library/pubsub/pubsub.lib.ts @@ -1,3 +1,4 @@ +import { Notification } from '@db/entities/core/notification.entity'; import { config } from '@lib/helpers/config.helper'; import { Server } from 'socket.io'; import { SocketIOInstance } from './socketio.pubsub'; @@ -9,6 +10,14 @@ export enum PubSubEvent { export enum PubSubEventType { OwnerCreateStock = 'owner_create_stock', + OwnerGetTableLabel = 'owner_get_table_label', + + // Staff + StaffCreateStock = 'staff_create_stock', + StaffGetTableLabel = 'staff_get_table_label', + + // Customer + CustomerCreateOrder = 'customer_create_order', } export enum PubSubStatus { @@ -21,6 +30,7 @@ export enum PubSubStatus { export enum PubSubPayloadType { Dialog = 'dialog', Download = 'download', + Notification = 'notification', } interface IPubSubEventData { diff --git a/templates/error.hbs b/templates/error.hbs new file mode 100644 index 0000000..93a6611 --- /dev/null +++ b/templates/error.hbs @@ -0,0 +1,447 @@ + + + + + + + + + KeepPack Error + + + +
+
+
+ {{ code }} error + {{ message }} +
+ + + +
+ + + +
+ + + +
+
+ + + + \ No newline at end of file diff --git a/templates/iframe.hbs b/templates/iframe.hbs new file mode 100644 index 0000000..8c922d4 --- /dev/null +++ b/templates/iframe.hbs @@ -0,0 +1,26 @@ + + + + + + + + + + {{name}} + + + + + + + + diff --git a/templates/pdf/label-table.hbs b/templates/pdf/label-table.hbs new file mode 100644 index 0000000..b8da7b4 --- /dev/null +++ b/templates/pdf/label-table.hbs @@ -0,0 +1,143 @@ + + + + + + Restaurant QR Code Menu + + + + {{#each data}} +
+
Ordero
+
+ QR Code +
Scan this QR
+
To create order
+
Table {{name}}
+
+ {{restaurant}}
+ Location: {{location}} +
+
+ + + + +
+
+ {{/each}} + + diff --git a/yarn.lock b/yarn.lock index 1392383..a5dce72 100644 --- a/yarn.lock +++ b/yarn.lock @@ -513,6 +513,18 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917" integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw== +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -960,6 +972,11 @@ consola "^2.15.0" node-fetch "^2.6.1" +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + "@selderee/plugin-htmlparser2@^0.6.0": version "0.6.0" resolved "https://registry.yarnpkg.com/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.6.0.tgz#27e994afd1c2cb647ceb5406a185a5574188069d" @@ -1038,10 +1055,10 @@ resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2" integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA== -"@sqltools/formatter@^1.2.2": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@sqltools/formatter/-/formatter-1.2.3.tgz#1185726610acc37317ddab11c3c7f9066966bd20" - integrity sha512-O3uyB/JbkAEMZaP3YqyHH7TMnex7tWyCbCI4EfJdOCoN6HIhqdJBWTM6aCCiWQ/5f5wxjgU735QAIpJbjDvmzg== +"@sqltools/formatter@^1.2.5": + version "1.2.5" + resolved "https://registry.yarnpkg.com/@sqltools/formatter/-/formatter-1.2.5.tgz#3abc203c79b8c3e90fd6c156a0c62d5403520e12" + integrity sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw== "@streamparser/json@^0.0.6": version "0.0.6" @@ -1732,6 +1749,13 @@ agent-base@6, agent-base@^6.0.0, agent-base@^6.0.2: dependencies: debug "4" +agent-base@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" + integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== + dependencies: + es6-promisify "^5.0.0" + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -1828,6 +1852,11 @@ ansi-styles@^6.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.1.0.tgz#87313c102b8118abd57371afab34618bf7350ed3" integrity sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ== +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + any-promise@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" @@ -1846,6 +1875,11 @@ app-root-path@^3.0.0: resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.0.0.tgz#210b6f43873227e18a4b810a032283311555d5ad" integrity sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw== +app-root-path@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.1.0.tgz#5971a2fc12ba170369a7a1ef018c71e6e47c2e86" + integrity sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA== + append-field@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" @@ -1946,6 +1980,11 @@ async-hook-jl@^1.7.6: dependencies: stack-chain "^1.3.7" +async-limiter@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + async@^3.2.3: version "3.2.4" resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" @@ -2190,6 +2229,11 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== + buffer-equal-constant-time@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" @@ -2225,6 +2269,11 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" +bwip-js@^3.0.2: + version "3.4.5" + resolved "https://registry.yarnpkg.com/bwip-js/-/bwip-js-3.4.5.tgz#e55a9ce5cd13a17b6f2b1370cda367741764d169" + integrity sha512-qeMBeeHkELC+ZTU3UjUFF0mkIQmhqKzo4B5fYfx98hitsdBMytSuAE9oVz2mE02GQ09u3QmgU/kke0qONumuGQ== + bytes@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" @@ -2493,6 +2542,15 @@ cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + clone@2.x: version "2.1.2" resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" @@ -2622,6 +2680,16 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +concat-stream@^1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + concat-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" @@ -2665,6 +2733,14 @@ content-disposition@^0.5.3: dependencies: safe-buffer "5.2.1" +convert-html-to-pdf@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/convert-html-to-pdf/-/convert-html-to-pdf-1.0.1.tgz#34f8af22cc0fbf1d134921d82c98e5dae16c6b6a" + integrity sha512-xHMmTgrSJwQ54s8pgZhr3nbQSJ59xN9LjE7L++71jViEGCAHy71XTd8C96Ez6czrcNO31Icx+K4pw9i0Df6gnA== + dependencies: + lodash "^4.17.15" + puppeteer "^1.19.0" + convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.8.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" @@ -2776,16 +2852,16 @@ data-uri-to-buffer@3: resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636" integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og== -date-fns@^2.28.0: - version "2.29.1" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.1.tgz#9667c2615525e552b5135a3116b95b1961456e60" - integrity sha512-dlLD5rKaKxpFdnjrs+5azHDFOPEu4ANy/LTh04A1DTzMM7qoajmKCBc8pkKRFT41CNzw+4gQh79X5C+Jq27HAw== - dayjs@^1.10.6, dayjs@^1.11.4: version "1.11.4" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.4.tgz#3b3c10ca378140d8917e06ebc13a4922af4f433e" integrity sha512-Zj/lPM5hOvQ1Bf7uAvewDaUcsJoI6JmNqmHhHl3nyumwe0XHwt8sWdOVAPACJzCebL8gQCi+K49w7iKWnGwX9g== +dayjs@^1.11.9: + version "1.11.11" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.11.tgz#dfe0e9d54c5f8b68ccf8ca5f72ac603e7e5ed59e" + integrity sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg== + dayjs@^1.8.29: version "1.11.10" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0" @@ -2798,14 +2874,14 @@ debug@2.6.9, debug@^2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: +debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" -debug@^3.2.7: +debug@^3.1.0, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== @@ -2900,7 +2976,7 @@ delegates@^1.0.0: resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== -denque@^2.0.1: +denque@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== @@ -3041,11 +3117,16 @@ domutils@^3.0.1: domelementtype "^2.3.0" domhandler "^5.0.1" -dotenv@^16.0.0, dotenv@^16.0.1: +dotenv@^16.0.1: version "16.0.1" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d" integrity sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ== +dotenv@^16.0.3: + version "16.4.5" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" + integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== + duplexify@^4.1.1, duplexify@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.2.tgz#18b4f8d28289132fa0b9573c898d9f903f81c7b0" @@ -3252,6 +3333,18 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +es6-promise@^4.0.3: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ== + dependencies: + es6-promise "^4.0.3" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -3594,6 +3687,16 @@ extract-css@^3.0.0: list-stylesheets "^2.0.0" style-data "^2.0.0" +extract-zip@^1.6.6: + version "1.7.0" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" + integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== + dependencies: + concat-stream "^1.6.2" + debug "^2.6.9" + mkdirp "^0.5.4" + yauzl "^2.10.0" + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -3733,6 +3836,13 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== + dependencies: + pend "~1.2.0" + fecha@^4.2.0: version "4.2.3" resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd" @@ -3856,6 +3966,14 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +foreground-child@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.2.1.tgz#767004ccf3a5b30df39bed90718bab43fe0a59f7" + integrity sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + fork-ts-checker-webpack-plugin@7.2.11: version "7.2.11" resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.2.11.tgz#aff3febbc11544ba3ad0ae4d5aa4055bd15cd26d" @@ -4101,7 +4219,18 @@ glob@8.0.3, glob@^8.0.1, glob@^8.0.3: minimatch "^5.0.1" once "^1.3.0" -glob@^7.0.0, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.2.0: +glob@^10.3.10: + version "10.4.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.1.tgz#0cfb01ab6a6b438177bfe6a58e2576f6efe909c2" + integrity sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + path-scurry "^1.11.1" + +glob@^7.0.0, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -4370,6 +4499,14 @@ https-proxy-agent@5, https-proxy-agent@^5.0.0: agent-base "6" debug "4" +https-proxy-agent@^2.2.1: + version "2.2.4" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" + integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== + dependencies: + agent-base "^4.3.0" + debug "^3.1.0" + human-signals@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" @@ -4827,6 +4964,15 @@ iterare@1.2.1: resolved "https://registry.yarnpkg.com/iterare/-/iterare-1.2.1.tgz#139c400ff7363690e33abffa33cbba8920f00042" integrity sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q== +jackspeak@^3.1.2: + version "3.4.0" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.0.tgz#a75763ff36ad778ede6a156d8ee8b124de445b4a" + integrity sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + jake@^10.8.5: version "10.8.5" resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" @@ -5606,17 +5752,22 @@ logform@^2.3.2, logform@^2.4.0: safe-stable-stringify "^2.3.1" triple-beam "^1.3.0" -long@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" - integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== +long@^5.2.1: + version "5.2.3" + resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" + integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== lower-case@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" integrity sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA== -lru-cache@^4.1.3, lru-cache@^4.1.5: +lru-cache@^10.2.0: + version "10.2.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.2.tgz#48206bc114c1252940c41b25b41af5b545aca878" + integrity sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ== + +lru-cache@^4.1.5: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== @@ -5638,6 +5789,16 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lru-cache@^7.14.1: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== + +lru-cache@^8.0.0: + version "8.0.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-8.0.5.tgz#983fe337f3e176667f8e567cfcce7cb064ea214e" + integrity sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA== + lru_map@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" @@ -5767,7 +5928,7 @@ mime@1.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@2.6.0, mime@^2.4.6: +mime@2.6.0, mime@^2.0.3, mime@^2.4.6: version "2.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== @@ -5806,6 +5967,13 @@ minimatch@^5.0.1: dependencies: brace-expansion "^2.0.1" +minimatch@^9.0.4: + version "9.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" + integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" @@ -5816,6 +5984,11 @@ minimist@^1.2.3: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + mjml-accordion@4.13.0: version "4.13.0" resolved "https://registry.yarnpkg.com/mjml-accordion/-/mjml-accordion-4.13.0.tgz#76ec0b5efe372e129077beaa0b2147c99d76eb7a" @@ -6153,11 +6326,23 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== +mkdirp@^0.5.4: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mkdirp@^2.1.3: + version "2.1.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-2.1.6.tgz#964fbcb12b2d8c5d6fbc62a963ac95a273e2cc19" + integrity sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A== + moo@^0.5.0, moo@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.1.tgz#7aae7f384b9b09f620b6abf6f74ebbcd1b65dbc4" @@ -6190,17 +6375,17 @@ mysql-import@^5.0.21: dependencies: mysql "^2.18.1" -mysql2@^2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-2.3.3.tgz#944f3deca4b16629052ff8614fbf89d5552545a0" - integrity sha512-wxJUev6LgMSgACDkb/InIFxDprRa6T95+VEoR+xPvtngtccNH2dGjEB/fVZ8yg1gWv1510c9CvXuJHi5zUm0ZA== +mysql2@^3.10.2: + version "3.10.2" + resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-3.10.2.tgz#37297d5d75d2958e37adc9cd4db865354832d7a4" + integrity sha512-KCXPEvAkO0RcHPr362O5N8tFY2fXvbjfkPvRY/wGumh4EOemo9Hm5FjQZqv/pCmrnuxGu5OxnSENG0gTXqKMgQ== dependencies: - denque "^2.0.1" + denque "^2.1.0" generate-function "^2.3.1" iconv-lite "^0.6.3" - long "^4.0.0" - lru-cache "^6.0.0" - named-placeholders "^1.1.2" + long "^5.2.1" + lru-cache "^8.0.0" + named-placeholders "^1.1.3" seq-queue "^0.0.5" sqlstring "^2.3.2" @@ -6223,12 +6408,12 @@ mz@^2.4.0: object-assign "^4.0.1" thenify-all "^1.0.0" -named-placeholders@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/named-placeholders/-/named-placeholders-1.1.2.tgz#ceb1fbff50b6b33492b5cf214ccf5e39cef3d0e8" - integrity sha512-wiFWqxoLL3PGVReSZpjLVxyJ1bRqe+KKJVbr4hGs1KWfTZTQyezHFBbuKj9hsizHyGV2ne7EMjHdxEGAybD5SA== +named-placeholders@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/named-placeholders/-/named-placeholders-1.1.3.tgz#df595799a36654da55dda6152ba7a137ad1d9351" + integrity sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w== dependencies: - lru-cache "^4.1.3" + lru-cache "^7.14.1" napi-build-utils@^1.0.1: version "1.0.2" @@ -6740,6 +6925,14 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-to-regexp@3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.2.0.tgz#fa7877ecbc495c601907562222453c43cc204a5f" @@ -6765,6 +6958,11 @@ peek-readable@^4.1.0: resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-4.1.0.tgz#4ece1111bf5c2ad8867c314c81356847e8a62e72" integrity sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg== +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== + pick-util@^1.1.4: version "1.1.5" resolved "https://registry.yarnpkg.com/pick-util/-/pick-util-1.1.5.tgz#514b11b1a49486d30c805a23125003a360175b6d" @@ -6969,6 +7167,11 @@ process-warning@^2.0.0: resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-2.0.0.tgz#341dbeaac985b90a04ebcd844d50097c7737b2ee" integrity sha512-+MmoAXoUX+VTHAlwns0h+kFUWFs/3FZy+ZuchkgjyOu3oioLAo2LB5aCfKPh2+P9O18i3m43tUEv3YqttSy0Ww== +progress@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + promise@^7.0.1: version "7.3.1" resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" @@ -7151,6 +7354,20 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +puppeteer@^1.19.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.20.0.tgz#e3d267786f74e1d87cf2d15acc59177f471bbe38" + integrity sha512-bt48RDBy2eIwZPrkgbcwHtb51mj2nKvHOPMaSH2IsWiv7lOG9k9zhaRzpDZafrk05ajMc3cu+lSQYYOfH2DkVQ== + dependencies: + debug "^4.1.0" + extract-zip "^1.6.6" + https-proxy-agent "^2.2.1" + mime "^2.0.3" + progress "^2.0.1" + proxy-from-env "^1.0.0" + rimraf "^2.6.1" + ws "^6.1.0" + q@2.0.x: version "2.0.3" resolved "https://registry.yarnpkg.com/q/-/q-2.0.3.tgz#75b8db0255a1a5af82f58c3f3aaa1efec7d0d134" @@ -7281,7 +7498,7 @@ readable-stream@2.3.7: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^2.0.6: +readable-stream@^2.0.6, readable-stream@^2.2.2: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -7346,6 +7563,11 @@ reflect-metadata@^0.1.13: resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== +reflect-metadata@^0.2.1: + version "0.2.2" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz#400c845b6cba87a21f2c65c4aeb158f4fa4d9c5b" + integrity sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q== + regenerator-runtime@^0.13.4: version "0.13.9" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" @@ -7460,6 +7682,13 @@ rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" +rimraf@^2.6.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + rootpath@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/rootpath/-/rootpath-0.1.2.tgz#5b379a87dca906e9b91d690a599439bef267ea6b" @@ -7732,6 +7961,11 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + simple-concat@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" @@ -7991,6 +8225,15 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -8009,7 +8252,7 @@ string-width@^1.0.1: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string-width@^5.0.0: +string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== @@ -8055,6 +8298,13 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" @@ -8477,7 +8727,7 @@ tsconfig-paths@4.0.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@2.4.0, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.1: +tslib@2.4.0, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.2.0: version "2.4.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== @@ -8492,6 +8742,11 @@ tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.5.0: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -8565,28 +8820,26 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typeorm@^0.3.7: - version "0.3.7" - resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.3.7.tgz#5776ed5058f0acb75d64723b39ff458d21de64c1" - integrity sha512-MsPJeP6Zuwfe64c++l80+VRqpGEGxf0CkztIEnehQ+CMmQPSHjOnFbFxwBuZ2jiLqZTjLk2ZqQdVF0RmvxNF3Q== +typeorm@^0.3.20: + version "0.3.20" + resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.3.20.tgz#4b61d737c6fed4e9f63006f88d58a5e54816b7ab" + integrity sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q== dependencies: - "@sqltools/formatter" "^1.2.2" - app-root-path "^3.0.0" + "@sqltools/formatter" "^1.2.5" + app-root-path "^3.1.0" buffer "^6.0.3" - chalk "^4.1.0" + chalk "^4.1.2" cli-highlight "^2.1.11" - date-fns "^2.28.0" - debug "^4.3.3" - dotenv "^16.0.0" - glob "^7.2.0" - js-yaml "^4.1.0" - mkdirp "^1.0.4" - reflect-metadata "^0.1.13" + dayjs "^1.11.9" + debug "^4.3.4" + dotenv "^16.0.3" + glob "^10.3.10" + mkdirp "^2.1.3" + reflect-metadata "^0.2.1" sha.js "^2.4.11" - tslib "^2.3.1" - uuid "^8.3.2" - xml2js "^0.4.23" - yargs "^17.3.1" + tslib "^2.5.0" + uuid "^9.0.0" + yargs "^17.6.2" typescript@4.7.4, typescript@^4.3.5: version "4.7.4" @@ -8705,6 +8958,11 @@ uuid@8.3.2, uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +uuid@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" @@ -8927,6 +9185,15 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" @@ -8945,6 +9212,15 @@ wrap-ansi@^7.0.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -8958,6 +9234,13 @@ write-file-atomic@^4.0.1: imurmurhash "^0.1.4" signal-exit "^3.0.7" +ws@^6.1.0: + version "6.2.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e" + integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw== + dependencies: + async-limiter "~1.0.0" + ws@~8.2.3: version "8.2.3" resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" @@ -8971,14 +9254,6 @@ xml2js@0.6.2: sax ">=0.6.0" xmlbuilder "~11.0.0" -xml2js@^0.4.23: - version "0.4.23" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" - integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== - dependencies: - sax ">=0.6.0" - xmlbuilder "~11.0.0" - xmlbuilder@^13.0.2: version "13.0.2" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-13.0.2.tgz#02ae33614b6a047d1c32b5389c1fdacb2bce47a7" @@ -9053,7 +9328,7 @@ yargs-parser@^20.2.2: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs-parser@^21.0.0, yargs-parser@^21.0.1: +yargs-parser@^21.0.0, yargs-parser@^21.0.1, yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== @@ -9084,6 +9359,27 @@ yargs@^17.3.1: y18n "^5.0.5" yargs-parser "^21.0.0" +yargs@^17.6.2: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + +yauzl@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" + yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"