Compare commits
No commits in common. "ee52a35af2d49c08f75daf8541842004a6eb03cf" and "5f08b4be66076ebf3fbc69becc148eed911cad72" have entirely different histories.
ee52a35af2
...
5f08b4be66
|
@ -101,9 +101,6 @@ import { BookingOnlineAuthModule } from './modules/booking-online/authentication
|
||||||
import { BookingOrderModule } from './modules/booking-online/order/order.module';
|
import { BookingOrderModule } from './modules/booking-online/order/order.module';
|
||||||
import { TimeGroupModule } from './modules/item-related/time-group/time-group.module';
|
import { TimeGroupModule } from './modules/item-related/time-group/time-group.module';
|
||||||
import { TimeGroupModel } from './modules/item-related/time-group/data/models/time-group.model';
|
import { TimeGroupModel } from './modules/item-related/time-group/data/models/time-group.model';
|
||||||
|
|
||||||
import { OtpVerificationModule } from './modules/configuration/otp-verification/otp-verification.module';
|
|
||||||
import { OtpVerificationModel } from './modules/configuration/otp-verification/data/models/otp-verification.model';
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ApmModule.register(),
|
ApmModule.register(),
|
||||||
|
@ -168,7 +165,6 @@ import { OtpVerificationModel } from './modules/configuration/otp-verification/d
|
||||||
|
|
||||||
// Booking Online
|
// Booking Online
|
||||||
VerificationModel,
|
VerificationModel,
|
||||||
OtpVerificationModel,
|
|
||||||
],
|
],
|
||||||
synchronize: false,
|
synchronize: false,
|
||||||
}),
|
}),
|
||||||
|
@ -234,8 +230,6 @@ import { OtpVerificationModel } from './modules/configuration/otp-verification/d
|
||||||
|
|
||||||
BookingOnlineAuthModule,
|
BookingOnlineAuthModule,
|
||||||
BookingOrderModule,
|
BookingOrderModule,
|
||||||
|
|
||||||
OtpVerificationModule,
|
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [
|
providers: [
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
interface OtpServiceEntity {
|
|
||||||
length?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class OtpService {
|
|
||||||
private readonly otpLength: number;
|
|
||||||
|
|
||||||
constructor({ length = 4 }: OtpServiceEntity) {
|
|
||||||
this.otpLength = Math.max(length, 4); // Minimum of 4 digits
|
|
||||||
}
|
|
||||||
|
|
||||||
private hasSequentialDigits(str: string): boolean {
|
|
||||||
for (let i = 0; i < str.length - 1; i++) {
|
|
||||||
const current = parseInt(str[i], 10);
|
|
||||||
const next = parseInt(str[i + 1], 10);
|
|
||||||
if (next === current + 1 || next === current - 1) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private hasRepeatedDigits(str: string): boolean {
|
|
||||||
return str.split('').every((char) => char === str[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private isPalindrome(str: string): boolean {
|
|
||||||
return str === str.split('').reverse().join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
private hasPartiallyRepeatedDigits(str: string): boolean {
|
|
||||||
const counts: Record<string, number> = {};
|
|
||||||
for (const char of str) {
|
|
||||||
counts[char] = (counts[char] || 0) + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reject if any digit appears more than twice
|
|
||||||
return Object.values(counts).some((count) => count > 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
public generateSecureOTP(): string {
|
|
||||||
let otp: string;
|
|
||||||
|
|
||||||
do {
|
|
||||||
otp = Array.from({ length: this.otpLength }, () =>
|
|
||||||
Math.floor(Math.random() * 10).toString(),
|
|
||||||
).join('');
|
|
||||||
} while (
|
|
||||||
this.hasSequentialDigits(otp) ||
|
|
||||||
this.hasRepeatedDigits(otp) ||
|
|
||||||
this.isPalindrome(otp) ||
|
|
||||||
this.hasPartiallyRepeatedDigits(otp)
|
|
||||||
);
|
|
||||||
|
|
||||||
return otp;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
||||||
|
|
||||||
export class CreateTableOtpCerifications1749028279580
|
|
||||||
implements MigrationInterface
|
|
||||||
{
|
|
||||||
name = 'CreateTableOtpCerifications1749028279580';
|
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(
|
|
||||||
`CREATE TYPE "public"."otp_verifications_action_type_enum" AS ENUM('CREATE_DISCOUNT', 'CANCEL_TRANSACTION')`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`CREATE TYPE "public"."otp_verifications_source_enum" AS ENUM('POS', 'WEB')`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`CREATE TABLE "otp_verifications" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "creator_id" character varying(36), "creator_name" character varying(125), "editor_id" character varying(36), "editor_name" character varying(125), "created_at" bigint NOT NULL, "updated_at" bigint NOT NULL, "otp_code" character varying NOT NULL, "action_type" "public"."otp_verifications_action_type_enum" NOT NULL, "target_id" character varying, "reference" character varying, "source" "public"."otp_verifications_source_enum" NOT NULL, "is_used" boolean NOT NULL DEFAULT false, "expired_at" bigint NOT NULL, "verified_at" bigint, CONSTRAINT "PK_91d17e75ac3182dba6701869b39" PRIMARY KEY ("id"))`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(`DROP TABLE "otp_verifications"`);
|
|
||||||
await queryRunner.query(
|
|
||||||
`DROP TYPE "public"."otp_verifications_source_enum"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
|
||||||
`DROP TYPE "public"."otp_verifications_action_type_enum"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
||||||
|
|
||||||
export class AddColumnIsReplacedOtpVerification1749030419440
|
|
||||||
implements MigrationInterface
|
|
||||||
{
|
|
||||||
name = 'AddColumnIsReplacedOtpVerification1749030419440';
|
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "otp_verifications" ADD "is_replaced" boolean NOT NULL DEFAULT false`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "otp_verifications" DROP COLUMN "is_replaced"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { TABLE_NAME } from 'src/core/strings/constants/table.constants';
|
import { TABLE_NAME } from 'src/core/strings/constants/table.constants';
|
||||||
import {
|
import {
|
||||||
OTP_ACTION_TYPE,
|
OPT_ACTION_TYPE,
|
||||||
OTP_SOURCE,
|
OTP_SOURCE,
|
||||||
OtpVerificationEntity,
|
OtpVerificationEntity,
|
||||||
} from '../../domain/entities/otp-verification.entity';
|
} from '../../domain/entities/otp-verification.entity';
|
||||||
|
@ -15,24 +15,18 @@ export class OtpVerificationModel
|
||||||
@Column({ type: 'varchar', nullable: false })
|
@Column({ type: 'varchar', nullable: false })
|
||||||
otp_code: string;
|
otp_code: string;
|
||||||
|
|
||||||
@Column({ type: 'enum', enum: OTP_ACTION_TYPE })
|
@Column({ type: 'enum', enum: OPT_ACTION_TYPE })
|
||||||
action_type: OTP_ACTION_TYPE;
|
action_type: OPT_ACTION_TYPE;
|
||||||
|
|
||||||
@Column({ type: 'varchar', nullable: true })
|
@Column({ type: 'varchar', nullable: true })
|
||||||
target_id: string;
|
target_id: string;
|
||||||
|
|
||||||
@Column({ type: 'varchar', nullable: true })
|
|
||||||
reference: string;
|
|
||||||
|
|
||||||
@Column({ type: 'enum', enum: OTP_SOURCE })
|
@Column({ type: 'enum', enum: OTP_SOURCE })
|
||||||
source: OTP_SOURCE;
|
source: OTP_SOURCE;
|
||||||
|
|
||||||
@Column({ default: false })
|
@Column({ default: false })
|
||||||
is_used: boolean;
|
is_used: boolean;
|
||||||
|
|
||||||
@Column({ default: false })
|
|
||||||
is_replaced: boolean;
|
|
||||||
|
|
||||||
@Column({ type: 'bigint', nullable: false })
|
@Column({ type: 'bigint', nullable: false })
|
||||||
expired_at: number; // UNIX timestamp
|
expired_at: number; // UNIX timestamp
|
||||||
|
|
||||||
|
|
|
@ -1,156 +0,0 @@
|
||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { OtpVerificationModel } from '../models/otp-verification.model';
|
|
||||||
import {
|
|
||||||
OTP_SOURCE,
|
|
||||||
OtpRequestEntity,
|
|
||||||
OtpVerificationEntity,
|
|
||||||
OtpVerifyEntity,
|
|
||||||
} from '../../domain/entities/otp-verification.entity';
|
|
||||||
import * as moment from 'moment';
|
|
||||||
import { OtpService } from 'src/core/helpers/otp/otp-service';
|
|
||||||
import { TABLE_NAME } from 'src/core/strings/constants/table.constants';
|
|
||||||
@Injectable()
|
|
||||||
export class OtpVerificationService {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(OtpVerificationModel)
|
|
||||||
private readonly otpVerificationRepo: Repository<OtpVerificationModel>,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
private generateOtpExpiration(minutes = 5): number {
|
|
||||||
return moment().add(minutes, 'minutes').valueOf(); // epoch millis expired time
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateResendAvailableAt(seconds = 90): number {
|
|
||||||
return moment().add(seconds, 'seconds').valueOf(); // epoch millis
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateTimestamp(): number {
|
|
||||||
return moment().valueOf(); // epoch millis verification time (now)
|
|
||||||
}
|
|
||||||
|
|
||||||
async requestOTP(payload: OtpRequestEntity) {
|
|
||||||
const otpService = new OtpService({ length: 4 });
|
|
||||||
const otpCode = otpService.generateSecureOTP();
|
|
||||||
const dateNow = this.generateTimestamp();
|
|
||||||
const expiredAt = this.generateOtpExpiration();
|
|
||||||
|
|
||||||
//TODO implementation from auth
|
|
||||||
const creator = { id: null, name: null };
|
|
||||||
|
|
||||||
const newOtp: OtpVerificationEntity = {
|
|
||||||
otp_code: otpCode,
|
|
||||||
action_type: payload.action_type,
|
|
||||||
target_id: payload.target_id,
|
|
||||||
reference: payload.reference,
|
|
||||||
source: payload.source,
|
|
||||||
is_used: false,
|
|
||||||
is_replaced: false,
|
|
||||||
expired_at: expiredAt,
|
|
||||||
|
|
||||||
creator_id: creator.id,
|
|
||||||
creator_name: creator.name,
|
|
||||||
created_at: dateNow,
|
|
||||||
verified_at: null,
|
|
||||||
|
|
||||||
editor_id: creator.id,
|
|
||||||
editor_name: creator.name,
|
|
||||||
updated_at: dateNow,
|
|
||||||
};
|
|
||||||
|
|
||||||
const activeOTP = await this.getActiveOtp(
|
|
||||||
payload.target_id ?? payload.reference,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (activeOTP) {
|
|
||||||
const createdAtMoment = moment(Number(activeOTP.created_at));
|
|
||||||
const nowMoment = moment(Number(dateNow));
|
|
||||||
const diffSeconds = nowMoment.diff(createdAtMoment, 'seconds');
|
|
||||||
if (diffSeconds < 90) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
'An active OTP request was made recently. Please try again later.',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Update data is_replaced on database
|
|
||||||
this.otpVerificationRepo.save({
|
|
||||||
...activeOTP,
|
|
||||||
is_replaced: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// save otp to database
|
|
||||||
await this.otpVerificationRepo.save(newOtp);
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: `OTP has been sent to the admin's WhatsApp.`,
|
|
||||||
updated_at: expiredAt,
|
|
||||||
resend_available_at: this.generateResendAvailableAt(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyOTP(payload: OtpVerifyEntity) {
|
|
||||||
const { otp_code, action_type, target_id, reference } = payload;
|
|
||||||
const dateNow = this.generateTimestamp();
|
|
||||||
|
|
||||||
if (!target_id && !reference) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
'Either target_id or reference must be provided.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build a where condition with OR between target_id and reference
|
|
||||||
const otp = await this.otpVerificationRepo.findOne({
|
|
||||||
where: [
|
|
||||||
{
|
|
||||||
otp_code,
|
|
||||||
action_type,
|
|
||||||
target_id,
|
|
||||||
is_used: false,
|
|
||||||
is_replaced: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
otp_code,
|
|
||||||
action_type,
|
|
||||||
reference,
|
|
||||||
is_used: false,
|
|
||||||
is_replaced: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!otp) {
|
|
||||||
throw new BadRequestException('Invalid or expired OTP.');
|
|
||||||
} else if (otp.expired_at <= dateNow) {
|
|
||||||
throw new BadRequestException('OTP has expired.');
|
|
||||||
}
|
|
||||||
|
|
||||||
otp.is_used = true;
|
|
||||||
otp.verified_at = dateNow;
|
|
||||||
|
|
||||||
// update otp to database
|
|
||||||
await this.otpVerificationRepo.save(otp);
|
|
||||||
return { message: 'OTP verified successfully.' };
|
|
||||||
}
|
|
||||||
|
|
||||||
async getActiveOtp(payload: string) {
|
|
||||||
const now = this.generateTimestamp();
|
|
||||||
const tableName = TABLE_NAME.OTP_VERIFICATIONS;
|
|
||||||
|
|
||||||
return this.otpVerificationRepo
|
|
||||||
.createQueryBuilder(tableName)
|
|
||||||
.where(
|
|
||||||
`(${tableName}.target_id = :payload OR ${tableName}.reference = :payload)
|
|
||||||
AND ${tableName}.is_used = false
|
|
||||||
AND ${tableName}.is_replaced = false
|
|
||||||
AND ${tableName}.expired_at > :now`,
|
|
||||||
{ payload, now },
|
|
||||||
)
|
|
||||||
.orderBy(
|
|
||||||
`CASE WHEN ${tableName}.target_id = :payload THEN 0 ELSE 1 END`,
|
|
||||||
'ASC',
|
|
||||||
)
|
|
||||||
.getOne();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { BaseEntity } from 'src/core/modules/domain/entities//base.entity';
|
import { BaseEntity } from 'src/core/modules/domain/entities//base.entity';
|
||||||
|
|
||||||
export enum OTP_ACTION_TYPE {
|
export enum OPT_ACTION_TYPE {
|
||||||
CREATE_DISCOUNT = 'CREATE_DISCOUNT',
|
CREATE_DISCOUNT = 'CREATE_DISCOUNT',
|
||||||
CANCEL_TRANSACTION = 'CANCEL_TRANSACTION',
|
CANCEL_TRANSACTION = 'CANCEL_TRANSACTION',
|
||||||
}
|
}
|
||||||
|
@ -12,23 +12,10 @@ export enum OTP_SOURCE {
|
||||||
|
|
||||||
export interface OtpVerificationEntity extends BaseEntity {
|
export interface OtpVerificationEntity extends BaseEntity {
|
||||||
otp_code: string;
|
otp_code: string;
|
||||||
action_type: OTP_ACTION_TYPE;
|
action_type: OPT_ACTION_TYPE;
|
||||||
target_id: string;
|
target_id: string;
|
||||||
reference: string;
|
|
||||||
source: OTP_SOURCE;
|
source: OTP_SOURCE;
|
||||||
is_used: boolean;
|
is_used: boolean;
|
||||||
is_replaced: boolean;
|
|
||||||
expired_at: number;
|
expired_at: number;
|
||||||
verified_at: number;
|
verified_at: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OtpRequestEntity {
|
|
||||||
action_type: OTP_ACTION_TYPE;
|
|
||||||
source: OTP_SOURCE;
|
|
||||||
target_id: string;
|
|
||||||
reference: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OtpVerifyEntity extends OtpRequestEntity {
|
|
||||||
otp_code: string;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
import { IsNotEmpty, IsString, ValidateIf } from 'class-validator';
|
|
||||||
import {
|
|
||||||
OTP_ACTION_TYPE,
|
|
||||||
OTP_SOURCE,
|
|
||||||
OtpRequestEntity,
|
|
||||||
OtpVerifyEntity,
|
|
||||||
} from '../../domain/entities/otp-verification.entity';
|
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
|
||||||
|
|
||||||
export class OtpRequestDto implements OtpRequestEntity {
|
|
||||||
@ApiProperty({
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
example: OTP_ACTION_TYPE.CANCEL_TRANSACTION,
|
|
||||||
description: 'CANCEL_TRANSACTION || CREATE_DISCOUNT',
|
|
||||||
})
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
action_type: OTP_ACTION_TYPE;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
example: OTP_SOURCE.POS,
|
|
||||||
description: 'POS || WEB',
|
|
||||||
})
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
source: OTP_SOURCE;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
name: 'target_id',
|
|
||||||
example: 'bccc0c6a-51a0-437f-abc8-dc18851604ee',
|
|
||||||
})
|
|
||||||
@IsString()
|
|
||||||
@ValidateIf((body) => body.target_id)
|
|
||||||
target_id: string;
|
|
||||||
|
|
||||||
@ApiProperty({ name: 'reference', example: '0625N21' })
|
|
||||||
@IsString()
|
|
||||||
@ValidateIf((body) => body.reference)
|
|
||||||
reference: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class OtpVerifyDto extends OtpRequestDto implements OtpVerifyEntity {
|
|
||||||
@ApiProperty({
|
|
||||||
name: 'otp_code',
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
example: '2345',
|
|
||||||
})
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
otp_code: string;
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
|
||||||
import { Public } from 'src/core/guards';
|
|
||||||
import { MODULE_NAME } from 'src/core/strings/constants/module.constants';
|
|
||||||
import { OtpVerificationService } from '../data/services/otp-verification.service';
|
|
||||||
import { OtpRequestDto, OtpVerifyDto } from './dto/otp-verification.dto';
|
|
||||||
|
|
||||||
//TODO implementation auth
|
|
||||||
@ApiTags(`${MODULE_NAME.OTP_VERIFICATIONS.split('-').join(' ')} - data`)
|
|
||||||
@Controller(`v1/${MODULE_NAME.OTP_VERIFICATIONS}`)
|
|
||||||
@Public()
|
|
||||||
export class OtpVerificationController {
|
|
||||||
constructor(
|
|
||||||
private readonly otpVerificationService: OtpVerificationService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Post('request')
|
|
||||||
async request(@Body() body: OtpRequestDto) {
|
|
||||||
return await this.otpVerificationService.requestOTP(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('verify')
|
|
||||||
async verify(@Body() body: OtpVerifyDto) {
|
|
||||||
return await this.otpVerificationService.verifyOTP(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get(':ref_or_target_id')
|
|
||||||
async getByPhoneNumber(@Param('ref_or_target_id') ref_or_target_id: string) {
|
|
||||||
return this.otpVerificationService.getActiveOtp(ref_or_target_id);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants';
|
|
||||||
|
|
||||||
import { ConfigModule } from '@nestjs/config';
|
|
||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { OtpVerificationModel } from './data/models/otp-verification.model';
|
|
||||||
import { OtpVerificationController } from './infrastructure/otp-verification-data.controller';
|
|
||||||
import { OtpVerificationService } from './data/services/otp-verification.service';
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
ConfigModule.forRoot(),
|
|
||||||
TypeOrmModule.forFeature([OtpVerificationModel], CONNECTION_NAME.DEFAULT),
|
|
||||||
],
|
|
||||||
controllers: [OtpVerificationController],
|
|
||||||
providers: [OtpVerificationService],
|
|
||||||
})
|
|
||||||
export class OtpVerificationModule {}
|
|
|
@ -26,7 +26,6 @@ export class DetailItemManager extends BaseDetailManager<ItemEntity> {
|
||||||
selectRelations: [
|
selectRelations: [
|
||||||
'item_category',
|
'item_category',
|
||||||
'bundling_items',
|
'bundling_items',
|
||||||
'bundling_items.time_group bundling_time_groups',
|
|
||||||
'tenant',
|
'tenant',
|
||||||
'time_group',
|
'time_group',
|
||||||
],
|
],
|
||||||
|
@ -65,9 +64,6 @@ export class DetailItemManager extends BaseDetailManager<ItemEntity> {
|
||||||
'bundling_items.hpp',
|
'bundling_items.hpp',
|
||||||
'bundling_items.base_price',
|
'bundling_items.base_price',
|
||||||
|
|
||||||
'bundling_time_groups.id',
|
|
||||||
'bundling_time_groups.name',
|
|
||||||
|
|
||||||
'tenant.id',
|
'tenant.id',
|
||||||
'tenant.name',
|
'tenant.name',
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue