get subscription in admin
This commit is contained in:
@@ -102,7 +102,9 @@ export class AuthService {
|
||||
this.configService.get<string>('JWT_REFRESH_EXPIRES_IN') ?? '7d';
|
||||
}
|
||||
|
||||
async requestOtp(mobile: string): Promise<{ message: string; code?: string }> {
|
||||
async requestOtp(
|
||||
mobile: string,
|
||||
): Promise<{ message: string; code?: string }> {
|
||||
const code = await this.sendOtp(mobile, this.otpExpiresInMinutes);
|
||||
return this.buildOtpRequestResponse(code);
|
||||
}
|
||||
@@ -114,9 +116,7 @@ export class AuthService {
|
||||
return this.requestOtp(mobile);
|
||||
}
|
||||
|
||||
async checkSalonMobile(
|
||||
mobile: string,
|
||||
): Promise<{ isRegistered: boolean }> {
|
||||
async checkSalonMobile(mobile: string): Promise<{ isRegistered: boolean }> {
|
||||
await this.ensureSalonMobileCanProceed(mobile);
|
||||
const isRegistered = await this.isSalonOwnerRegistered(mobile);
|
||||
return { isRegistered };
|
||||
|
||||
@@ -5,7 +5,8 @@ export class RequestOtpResponseDto {
|
||||
message: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Only returned in non-production environments when SMS is unavailable',
|
||||
description:
|
||||
'Only returned in non-production environments when SMS is unavailable',
|
||||
example: '12345',
|
||||
})
|
||||
code?: string;
|
||||
|
||||
@@ -16,7 +16,8 @@ export class RequestSalonOtpResponseDto {
|
||||
expiresInSeconds: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Only returned in non-production environments when SMS is unavailable',
|
||||
description:
|
||||
'Only returned in non-production environments when SMS is unavailable',
|
||||
example: '12345',
|
||||
})
|
||||
code?: string;
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
Length,
|
||||
Matches,
|
||||
} from 'class-validator';
|
||||
import { IsOptional, IsString, IsUUID, Length, Matches } from 'class-validator';
|
||||
|
||||
export class VerifySalonOtpDto {
|
||||
@Matches(/^09\d{9}$/)
|
||||
|
||||
@@ -10,7 +10,10 @@ import { AuthorizationService } from '../authorization.service';
|
||||
export class ReservePolicy {
|
||||
constructor(private readonly authorizationService: AuthorizationService) {}
|
||||
|
||||
canCreate(user: AuthUser, reserve: Pick<Reserve, 'salon' | 'customer'>): boolean {
|
||||
canCreate(
|
||||
user: AuthUser,
|
||||
reserve: Pick<Reserve, 'salon' | 'customer'>,
|
||||
): boolean {
|
||||
if (this.authorizationService.isAdmin(user)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -20,9 +20,7 @@ export class StylistPolicy {
|
||||
if (this.authorizationService.isAdmin(user)) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
user.role === UserRole.SALON && stylist.salon.ownerId === user.id
|
||||
);
|
||||
return user.role === UserRole.SALON && stylist.salon.ownerId === user.id;
|
||||
}
|
||||
|
||||
canDelete(user: AuthUser, stylist: Stylist): boolean {
|
||||
|
||||
@@ -20,7 +20,9 @@ export class ApiExceptionFilter implements ExceptionFilter {
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
if (SWAGGER_PATH_PREFIXES.some((prefix) => request.url.startsWith(prefix))) {
|
||||
if (
|
||||
SWAGGER_PATH_PREFIXES.some((prefix) => request.url.startsWith(prefix))
|
||||
) {
|
||||
if (exception instanceof HttpException) {
|
||||
response.status(exception.getStatus()).json(exception.getResponse());
|
||||
return;
|
||||
@@ -67,8 +69,7 @@ export class ApiExceptionFilter implements ExceptionFilter {
|
||||
errors = responseMessage.filter(
|
||||
(item): item is string => typeof item === 'string',
|
||||
);
|
||||
message =
|
||||
errors.length > 0 ? 'Validation failed' : 'Bad Request';
|
||||
message = errors.length > 0 ? 'Validation failed' : 'Bad Request';
|
||||
} else if (typeof responseMessage === 'string') {
|
||||
message = responseMessage;
|
||||
} else if (typeof responseBody.error === 'string') {
|
||||
|
||||
@@ -33,29 +33,30 @@ function isPaginatedResult<T>(value: unknown): value is PaginatedResult<T> {
|
||||
);
|
||||
}
|
||||
|
||||
function isMessageOnlyResponse(
|
||||
value: unknown,
|
||||
): value is { message: string } {
|
||||
function isMessageOnlyResponse(value: unknown): value is { message: string } {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
'message' in value &&
|
||||
typeof (value as { message: unknown }).message === 'string' &&
|
||||
typeof value.message === 'string' &&
|
||||
Object.keys(value).length === 1
|
||||
);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class TransformResponseInterceptor<T>
|
||||
implements NestInterceptor<T, ApiSuccessResponse<T>>
|
||||
{
|
||||
export class TransformResponseInterceptor<T> implements NestInterceptor<
|
||||
T,
|
||||
ApiSuccessResponse<T>
|
||||
> {
|
||||
intercept(
|
||||
context: ExecutionContext,
|
||||
next: CallHandler<T>,
|
||||
): Observable<ApiSuccessResponse<T>> {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
|
||||
if (SWAGGER_PATH_PREFIXES.some((prefix) => request.url.startsWith(prefix))) {
|
||||
if (
|
||||
SWAGGER_PATH_PREFIXES.some((prefix) => request.url.startsWith(prefix))
|
||||
) {
|
||||
return next.handle() as Observable<ApiSuccessResponse<T>>;
|
||||
}
|
||||
|
||||
|
||||
@@ -349,8 +349,7 @@ export class CustomersService {
|
||||
!search ||
|
||||
fullName.includes(search) ||
|
||||
normalizedMobile.includes(search);
|
||||
const matchesStatus =
|
||||
!query.status || customer.status === query.status;
|
||||
const matchesStatus = !query.status || customer.status === query.status;
|
||||
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
import { Permissions } from '../auth/decorators/permissions.decorator';
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import {
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { AuthUser } from '../auth/interfaces/auth-user.interface';
|
||||
@@ -272,11 +276,16 @@ export class DashboardService {
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private buildRevenueChart(reserves: Reserve[], referenceDate: Date): number[] {
|
||||
private buildRevenueChart(
|
||||
reserves: Reserve[],
|
||||
referenceDate: Date,
|
||||
): number[] {
|
||||
const chart: number[] = [];
|
||||
|
||||
for (let offset = 11; offset >= 0; offset -= 1) {
|
||||
const monthDate = this.startOfMonth(this.addMonths(referenceDate, -offset));
|
||||
const monthDate = this.startOfMonth(
|
||||
this.addMonths(referenceDate, -offset),
|
||||
);
|
||||
const nextMonth = this.startOfNextMonth(monthDate);
|
||||
chart.push(this.sumIncomeInRange(reserves, monthDate, nextMonth));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
|
||||
import {
|
||||
MigrationInterface,
|
||||
QueryRunner,
|
||||
Table,
|
||||
TableForeignKey,
|
||||
} from 'typeorm';
|
||||
|
||||
export class InitialSchema1719600000000 implements MigrationInterface {
|
||||
name = 'InitialSchema1719600000000';
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
|
||||
import {
|
||||
MigrationInterface,
|
||||
QueryRunner,
|
||||
Table,
|
||||
TableForeignKey,
|
||||
} from 'typeorm';
|
||||
|
||||
export class AddStylists1719700000000 implements MigrationInterface {
|
||||
name = 'AddStylists1719700000000';
|
||||
@@ -17,7 +22,12 @@ export class AddStylists1719700000000 implements MigrationInterface {
|
||||
},
|
||||
{ name: 'salonId', type: 'uuid' },
|
||||
{ name: 'userId', type: 'uuid', isUnique: true },
|
||||
{ name: 'avatarUrl', type: 'varchar', length: '500', isNullable: true },
|
||||
{
|
||||
name: 'avatarUrl',
|
||||
type: 'varchar',
|
||||
length: '500',
|
||||
isNullable: true,
|
||||
},
|
||||
{ name: 'experienceYears', type: 'int', default: 0 },
|
||||
{ name: 'createdAt', type: 'timestamp', default: 'now()' },
|
||||
{ name: 'updatedAt', type: 'timestamp', default: 'now()' },
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
|
||||
import {
|
||||
MigrationInterface,
|
||||
QueryRunner,
|
||||
Table,
|
||||
TableForeignKey,
|
||||
} from 'typeorm';
|
||||
|
||||
export class AddServices1719800000000 implements MigrationInterface {
|
||||
name = 'AddServices1719800000000';
|
||||
@@ -17,7 +22,12 @@ export class AddServices1719800000000 implements MigrationInterface {
|
||||
},
|
||||
{ name: 'title', type: 'varchar', length: '200' },
|
||||
{ name: 'iconUrl', type: 'varchar', length: '500', isNullable: true },
|
||||
{ name: 'coverUrl', type: 'varchar', length: '500', isNullable: true },
|
||||
{
|
||||
name: 'coverUrl',
|
||||
type: 'varchar',
|
||||
length: '500',
|
||||
isNullable: true,
|
||||
},
|
||||
{ name: 'createdAt', type: 'timestamp', default: 'now()' },
|
||||
{ name: 'updatedAt', type: 'timestamp', default: 'now()' },
|
||||
],
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
MigrationInterface,
|
||||
QueryRunner,
|
||||
Table,
|
||||
TableIndex,
|
||||
} from 'typeorm';
|
||||
import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm';
|
||||
|
||||
export class AddSmsTemplates1720100000000 implements MigrationInterface {
|
||||
name = 'AddSmsTemplates1720100000000';
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { MigrationInterface, QueryRunner, TableForeignKey, TableIndex } from 'typeorm';
|
||||
import {
|
||||
MigrationInterface,
|
||||
QueryRunner,
|
||||
TableForeignKey,
|
||||
TableIndex,
|
||||
} from 'typeorm';
|
||||
|
||||
export class ExtendPaymentsForSubscriptions1720700000000
|
||||
implements MigrationInterface
|
||||
{
|
||||
export class ExtendPaymentsForSubscriptions1720700000000 implements MigrationInterface {
|
||||
name = 'ExtendPaymentsForSubscriptions1720700000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
|
||||
@@ -7,9 +7,7 @@ import {
|
||||
TableUnique,
|
||||
} from 'typeorm';
|
||||
|
||||
export class MoveSuggestionsToServiceSkill1720900000000
|
||||
implements MigrationInterface
|
||||
{
|
||||
export class MoveSuggestionsToServiceSkill1720900000000 implements MigrationInterface {
|
||||
name = 'MoveSuggestionsToServiceSkill1720900000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
@@ -26,7 +24,10 @@ export class MoveSuggestionsToServiceSkill1720900000000
|
||||
fk.columnNames.includes('suggestedServiceId'),
|
||||
);
|
||||
if (suggestedServiceFk) {
|
||||
await queryRunner.dropForeignKey('service_suggestions', suggestedServiceFk);
|
||||
await queryRunner.dropForeignKey(
|
||||
'service_suggestions',
|
||||
suggestedServiceFk,
|
||||
);
|
||||
}
|
||||
|
||||
const uniqueConstraints = table?.uniques ?? [];
|
||||
@@ -36,7 +37,10 @@ export class MoveSuggestionsToServiceSkill1720900000000
|
||||
|
||||
const indexes = table?.indices ?? [];
|
||||
for (const index of indexes) {
|
||||
if (index.columnNames.includes('serviceId') || index.columnNames.includes('serviceSkillId')) {
|
||||
if (
|
||||
index.columnNames.includes('serviceId') ||
|
||||
index.columnNames.includes('serviceSkillId')
|
||||
) {
|
||||
await queryRunner.dropIndex('service_suggestions', index);
|
||||
}
|
||||
}
|
||||
@@ -128,7 +132,10 @@ export class MoveSuggestionsToServiceSkill1720900000000
|
||||
'IDX_service_suggestions_serviceSkillId_isActive_displayPriority',
|
||||
);
|
||||
await queryRunner.dropColumn('service_suggestions', 'serviceSkillId');
|
||||
await queryRunner.dropColumn('service_suggestions', 'suggestedServiceSkillId');
|
||||
await queryRunner.dropColumn(
|
||||
'service_suggestions',
|
||||
'suggestedServiceSkillId',
|
||||
);
|
||||
|
||||
await queryRunner.addColumn(
|
||||
'service_suggestions',
|
||||
|
||||
@@ -202,10 +202,7 @@ export class PaymentsService {
|
||||
});
|
||||
}
|
||||
|
||||
async findByReserveId(
|
||||
reserveId: string,
|
||||
user: AuthUser,
|
||||
): Promise<Payment[]> {
|
||||
async findByReserveId(reserveId: string, user: AuthUser): Promise<Payment[]> {
|
||||
await this.reservesService.findOne(reserveId, user);
|
||||
|
||||
return this.paymentsRepository.find({
|
||||
@@ -313,10 +310,7 @@ export class PaymentsService {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
private buildSuccessReturnUrl(
|
||||
returnUrl: string,
|
||||
payment: Payment,
|
||||
): string {
|
||||
private buildSuccessReturnUrl(returnUrl: string, payment: Payment): string {
|
||||
return this.buildReturnUrl(returnUrl, {
|
||||
success: 'true',
|
||||
type: payment.type,
|
||||
|
||||
@@ -53,10 +53,7 @@ export class ReservesController {
|
||||
@Permissions(PermissionCode.RESERVES_READ)
|
||||
@ApiStandardOkResponse(Reserve, { isArray: true })
|
||||
@Get()
|
||||
findAll(
|
||||
@Query() query: FindReservesQueryDto,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
findAll(@Query() query: FindReservesQueryDto, @CurrentUser() user: AuthUser) {
|
||||
return this.reservesService.findAll(user, query);
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,10 @@ export class ReservesService {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
this.validateTimeRange(createReserveDto.startTime, createReserveDto.endTime);
|
||||
this.validateTimeRange(
|
||||
createReserveDto.startTime,
|
||||
createReserveDto.endTime,
|
||||
);
|
||||
this.validateAmounts(
|
||||
createReserveDto.totalAmount,
|
||||
createReserveDto.depositAmount,
|
||||
@@ -319,9 +322,7 @@ export class ReservesService {
|
||||
|
||||
private validateAmounts(totalAmount: number, depositAmount: number): void {
|
||||
if (depositAmount > totalAmount) {
|
||||
throw new BadRequestException(
|
||||
'depositAmount cannot exceed totalAmount',
|
||||
);
|
||||
throw new BadRequestException('depositAmount cannot exceed totalAmount');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,9 +330,7 @@ export class ReservesService {
|
||||
return time.length === 5 ? `${time}:00` : time;
|
||||
}
|
||||
|
||||
private async validateItems(
|
||||
items: CreateReserveDto['items'],
|
||||
): Promise<void> {
|
||||
private async validateItems(items: CreateReserveDto['items']): Promise<void> {
|
||||
for (const item of items) {
|
||||
const serviceExists = await this.servicesRepository.existsBy({
|
||||
id: item.serviceId,
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { IsNotEmpty, IsOptional, IsString, IsUrl, MaxLength } from 'class-validator';
|
||||
import {
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUrl,
|
||||
MaxLength,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateServiceDto {
|
||||
@IsString()
|
||||
|
||||
@@ -17,7 +17,9 @@ export class ServiceSkill {
|
||||
@Column()
|
||||
serviceId: string;
|
||||
|
||||
@ManyToOne(() => Service, (service) => service.skills, { onDelete: 'CASCADE' })
|
||||
@ManyToOne(() => Service, (service) => service.skills, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'serviceId' })
|
||||
service: Service;
|
||||
|
||||
|
||||
@@ -27,7 +27,9 @@ export class ServicesService {
|
||||
create(createServiceDto: CreateServiceDto, user: AuthUser): Promise<Service> {
|
||||
this.ensureCanManage(user);
|
||||
const service = this.servicesRepository.create(createServiceDto);
|
||||
return this.servicesRepository.save(service).then((saved) => this.findOne(saved.id));
|
||||
return this.servicesRepository
|
||||
.save(service)
|
||||
.then((saved) => this.findOne(saved.id));
|
||||
}
|
||||
|
||||
findAll(): Promise<Service[]> {
|
||||
|
||||
@@ -69,10 +69,7 @@ export class SmsTemplatesService {
|
||||
return extractPlaceholders(template.body);
|
||||
}
|
||||
|
||||
render(
|
||||
template: SmsTemplate,
|
||||
variables: Record<string, string>,
|
||||
): string {
|
||||
render(template: SmsTemplate, variables: Record<string, string>): string {
|
||||
return renderSmsTemplate(template.body, variables);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ export function renderSmsTemplate(
|
||||
body: string,
|
||||
variables: Record<string, string>,
|
||||
): string {
|
||||
return body.replace(PLACEHOLDER_PATTERN, (_, key: string) =>
|
||||
variables[key] ?? `{${key}}`,
|
||||
return body.replace(
|
||||
PLACEHOLDER_PATTERN,
|
||||
(_, key: string) => variables[key] ?? `{${key}}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,10 @@ export class UpdateSalonStylistDto {
|
||||
@Min(0)
|
||||
experienceYears?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: 'https://example.com/avatar.jpg', nullable: true })
|
||||
@ApiPropertyOptional({
|
||||
example: 'https://example.com/avatar.jpg',
|
||||
nullable: true,
|
||||
})
|
||||
@IsOptional()
|
||||
@ValidateIf((_, value) => value !== null)
|
||||
@IsString()
|
||||
|
||||
@@ -117,7 +117,9 @@ export class StylistsController {
|
||||
@ApiQuery({ name: 'salonId', required: false, type: String, format: 'uuid' })
|
||||
@ApiStandardOkResponse(Stylist, { isArray: true })
|
||||
@Get()
|
||||
findAll(@Query('salonId', new ParseUUIDPipe({ optional: true })) salonId?: string) {
|
||||
findAll(
|
||||
@Query('salonId', new ParseUUIDPipe({ optional: true })) salonId?: string,
|
||||
) {
|
||||
return this.stylistsService.findAll(salonId);
|
||||
}
|
||||
|
||||
|
||||
@@ -114,9 +114,7 @@ export class StylistsService {
|
||||
return stylists.filter((stylist) => {
|
||||
const fullName =
|
||||
`${stylist.user.firstName} ${stylist.user.lastName}`.toLowerCase();
|
||||
return (
|
||||
fullName.includes(search) || stylist.user.mobile.includes(search)
|
||||
);
|
||||
return fullName.includes(search) || stylist.user.mobile.includes(search);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -214,9 +212,7 @@ export class StylistsService {
|
||||
}
|
||||
|
||||
if (existingUser.role === UserRole.ADMIN) {
|
||||
throw new ConflictException(
|
||||
'Admins cannot be registered as stylists',
|
||||
);
|
||||
throw new ConflictException('Admins cannot be registered as stylists');
|
||||
}
|
||||
|
||||
const existingStylist = await this.stylistsRepository.findOne({
|
||||
|
||||
@@ -65,6 +65,15 @@ export class SalonSubscriptionsController {
|
||||
return this.salonSubscriptionsService.purchaseSmsPackage(dto, user);
|
||||
}
|
||||
|
||||
@Roles(UserRole.ADMIN, UserRole.SALON)
|
||||
@Permissions(PermissionCode.SUBSCRIPTIONS_READ)
|
||||
@AllowWithoutSubscription()
|
||||
@ApiStandardOkResponse(SalonSubscription, { isArray: true })
|
||||
@Get()
|
||||
findAll(@CurrentUser() user: AuthUser) {
|
||||
return this.salonSubscriptionsService.findAll(user);
|
||||
}
|
||||
|
||||
@Roles(UserRole.ADMIN, UserRole.SALON)
|
||||
@Permissions(PermissionCode.SUBSCRIPTIONS_READ)
|
||||
@AllowWithoutSubscription()
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Repository } from 'typeorm';
|
||||
import { AuthUser } from '../auth/interfaces/auth-user.interface';
|
||||
import { SubscriptionPolicy } from '../authorization/policies/subscription.policy';
|
||||
import { SalonsService } from '../salons/salons.service';
|
||||
import { UserRole } from '../users/enums/user-role.enum';
|
||||
import { SMS_ROLLOVER_GRACE_DAYS } from './constants/subscription.constants';
|
||||
import { PurchaseSmsPackageDto } from './dto/purchase-sms-package.dto';
|
||||
import { SalonSmsPurchase } from './entities/salon-sms-purchase.entity';
|
||||
@@ -126,6 +127,23 @@ export class SalonSubscriptionsService {
|
||||
});
|
||||
}
|
||||
|
||||
async findAll(user: AuthUser): Promise<SalonSubscription[]> {
|
||||
if (user.role === UserRole.SALON) {
|
||||
return this.subscriptionsRepository
|
||||
.createQueryBuilder('subscription')
|
||||
.innerJoinAndSelect('subscription.salon', 'salon')
|
||||
.innerJoinAndSelect('subscription.plan', 'plan')
|
||||
.where('salon.ownerId = :ownerId', { ownerId: user.id })
|
||||
.orderBy('subscription.createdAt', 'DESC')
|
||||
.getMany();
|
||||
}
|
||||
|
||||
return this.subscriptionsRepository.find({
|
||||
relations: { plan: true, salon: true },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async findActiveForSalon(
|
||||
salonId: string,
|
||||
user: AuthUser,
|
||||
@@ -138,7 +156,10 @@ export class SalonSubscriptionsService {
|
||||
return this.findActiveSubscription(salonId);
|
||||
}
|
||||
|
||||
async getSmsBalance(salonId: string, user: AuthUser): Promise<SalonSmsBalance> {
|
||||
async getSmsBalance(
|
||||
salonId: string,
|
||||
user: AuthUser,
|
||||
): Promise<SalonSmsBalance> {
|
||||
const salon = await this.salonsService.findOne(salonId);
|
||||
if (!this.subscriptionPolicy.canRead(user, salon)) {
|
||||
throw new ForbiddenException();
|
||||
@@ -206,7 +227,9 @@ export class SalonSubscriptionsService {
|
||||
|
||||
const hasActive = await this.hasActiveSubscription(salonId);
|
||||
if (!hasActive) {
|
||||
throw new BadRequestException('Salon does not have an active subscription');
|
||||
throw new BadRequestException(
|
||||
'Salon does not have an active subscription',
|
||||
);
|
||||
}
|
||||
|
||||
let remaining = count;
|
||||
@@ -303,7 +326,10 @@ export class SalonSubscriptionsService {
|
||||
order: { endDate: 'DESC' },
|
||||
});
|
||||
|
||||
if (!lastSubscription || lastSubscription.status === SalonSubscriptionStatus.ACTIVE) {
|
||||
if (
|
||||
!lastSubscription ||
|
||||
lastSubscription.status === SalonSubscriptionStatus.ACTIVE
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { CreateSmsPackageDto } from './dto/create-sms-package.dto';
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { CreateSubscriptionPlanDto } from './dto/create-subscription-plan.dto';
|
||||
@@ -56,7 +53,8 @@ export class SubscriptionPlansService {
|
||||
}
|
||||
if (dto.name !== undefined) plan.name = dto.name;
|
||||
if (dto.code !== undefined) plan.code = dto.code;
|
||||
if (dto.durationMonths !== undefined) plan.durationMonths = dto.durationMonths;
|
||||
if (dto.durationMonths !== undefined)
|
||||
plan.durationMonths = dto.durationMonths;
|
||||
if (dto.freeSmsCount !== undefined) plan.freeSmsCount = dto.freeSmsCount;
|
||||
if (dto.isActive !== undefined) plan.isActive = dto.isActive;
|
||||
return this.plansRepository.save(plan);
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
ArrayMinSize,
|
||||
IsArray,
|
||||
IsUUID,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { ArrayMinSize, IsArray, IsUUID, ValidateNested } from 'class-validator';
|
||||
import { CreateBulkServiceSuggestionItemDto } from './create-bulk-service-suggestion-item.dto';
|
||||
|
||||
export class CreateBulkServiceSuggestionsDto {
|
||||
|
||||
@@ -124,11 +124,15 @@ export class SuggestionsService {
|
||||
): Promise<ServiceSuggestion> {
|
||||
this.ensureCanManage(user);
|
||||
const suggestion = await this.findOne(id);
|
||||
const serviceSkillId = updateDto.serviceSkillId ?? suggestion.serviceSkillId;
|
||||
const serviceSkillId =
|
||||
updateDto.serviceSkillId ?? suggestion.serviceSkillId;
|
||||
const suggestedServiceSkillId =
|
||||
updateDto.suggestedServiceSkillId ?? suggestion.suggestedServiceSkillId;
|
||||
|
||||
await this.validateSuggestionTargets(serviceSkillId, suggestedServiceSkillId);
|
||||
await this.validateSuggestionTargets(
|
||||
serviceSkillId,
|
||||
suggestedServiceSkillId,
|
||||
);
|
||||
Object.assign(suggestion, updateDto);
|
||||
await this.suggestionsRepository.save(suggestion);
|
||||
return this.findOne(id);
|
||||
@@ -175,14 +179,20 @@ export class SuggestionsService {
|
||||
]);
|
||||
}
|
||||
|
||||
private async validateBaseServiceSkill(serviceSkillId: string): Promise<void> {
|
||||
const exists = await this.serviceSkillsRepository.existsBy({ id: serviceSkillId });
|
||||
private async validateBaseServiceSkill(
|
||||
serviceSkillId: string,
|
||||
): Promise<void> {
|
||||
const exists = await this.serviceSkillsRepository.existsBy({
|
||||
id: serviceSkillId,
|
||||
});
|
||||
if (!exists) {
|
||||
throw new NotFoundException(`Service skill #${serviceSkillId} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
private ensureUniqueSuggestedSkills(suggestedServiceSkillIds: string[]): void {
|
||||
private ensureUniqueSuggestedSkills(
|
||||
suggestedServiceSkillIds: string[],
|
||||
): void {
|
||||
const uniqueIds = new Set(suggestedServiceSkillIds);
|
||||
if (uniqueIds.size !== suggestedServiceSkillIds.length) {
|
||||
throw new BadRequestException('Duplicate suggested skills in batch');
|
||||
@@ -204,7 +214,9 @@ export class SuggestionsService {
|
||||
|
||||
if (suggestedSkills.length !== suggestedServiceSkillIds.length) {
|
||||
const foundIds = new Set(suggestedSkills.map((skill) => skill.id));
|
||||
const missingId = suggestedServiceSkillIds.find((id) => !foundIds.has(id));
|
||||
const missingId = suggestedServiceSkillIds.find(
|
||||
(id) => !foundIds.has(id),
|
||||
);
|
||||
throw new NotFoundException(
|
||||
`Suggested service skill #${missingId} not found`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user