get subscription in admin

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