trial
This commit is contained in:
@@ -304,10 +304,26 @@ export class AuthService {
|
|||||||
ownerId: user.id,
|
ownerId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.grantFreeTrial(salon.id, user.id);
|
||||||
|
|
||||||
const tokens = await this.issueTokens(user);
|
const tokens = await this.issueTokens(user);
|
||||||
return { ...tokens, salon };
|
return { ...tokens, salon };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async grantFreeTrial(
|
||||||
|
salonId: string,
|
||||||
|
ownerId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.salonSubscriptionsService.startFreeTrial(salonId, ownerId);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`Failed to grant free trial for salon ${salonId}`,
|
||||||
|
error instanceof Error ? error.stack : undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private validateSalonSignupFields(dto: VerifySalonOtpDto): void {
|
private validateSalonSignupFields(dto: VerifySalonOtpDto): void {
|
||||||
const requiredFields: Array<keyof VerifySalonOtpDto> = [
|
const requiredFields: Array<keyof VerifySalonOtpDto> = [
|
||||||
'firstName',
|
'firstName',
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
import { NotificationScenarioKey } from '../../notifications/enums/notification-scenario-key.enum';
|
||||||
|
|
||||||
|
const NEW_NOTIFICATION_KEYS = [NotificationScenarioKey.SUBSCRIPTION_EXPIRED];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only adds the new enum value. Postgres forbids using a newly added enum
|
||||||
|
* value in the same transaction it was created in, so seeding the actual
|
||||||
|
* scenario row is handled by the follow-up `SeedSubscriptionExpiredScenario`
|
||||||
|
* migration. `transaction = false` commits ADD VALUE immediately (required
|
||||||
|
* by Postgres).
|
||||||
|
*/
|
||||||
|
export class AddSubscriptionExpiredScenario1722500000000 implements MigrationInterface {
|
||||||
|
name = 'AddSubscriptionExpiredScenario1722500000000';
|
||||||
|
transaction = false;
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
for (const key of NEW_NOTIFICATION_KEYS) {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TYPE "notification_scenario_key_enum" ADD VALUE IF NOT EXISTS '${key}'`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(): Promise<void> {
|
||||||
|
// Enum values cannot be dropped without recreating the type; left as-is on downgrade.
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
import { NOTIFICATION_SCENARIOS } from '../../notifications/constants/notification-scenarios.constants';
|
||||||
|
import { NotificationScenarioKey } from '../../notifications/enums/notification-scenario-key.enum';
|
||||||
|
|
||||||
|
const NEW_NOTIFICATION_KEYS = [NotificationScenarioKey.SUBSCRIPTION_EXPIRED];
|
||||||
|
|
||||||
|
/** Seeds the default scenario row for the enum value added in AddSubscriptionExpiredScenario. */
|
||||||
|
export class SeedSubscriptionExpiredScenario1722600000000 implements MigrationInterface {
|
||||||
|
name = 'SeedSubscriptionExpiredScenario1722600000000';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
for (const key of NEW_NOTIFICATION_KEYS) {
|
||||||
|
const scenario = NOTIFICATION_SCENARIOS.find((s) => s.key === key);
|
||||||
|
if (!scenario) continue;
|
||||||
|
await queryRunner.query(
|
||||||
|
`
|
||||||
|
INSERT INTO notification_scenario_settings (key, title, body, "isActive")
|
||||||
|
VALUES ($1, $2, $3, true)
|
||||||
|
ON CONFLICT (key) DO NOTHING
|
||||||
|
`,
|
||||||
|
[scenario.key, scenario.defaultTitle, scenario.defaultBody],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`DELETE FROM "notification_scenario_settings" WHERE "key" IN (${NEW_NOTIFICATION_KEYS.map((key) => `'${key}'`).join(', ')})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
|
||||||
|
import {
|
||||||
|
FREE_TRIAL_DURATION_DAYS,
|
||||||
|
FREE_TRIAL_PLAN_CODE,
|
||||||
|
FREE_TRIAL_SMS_COUNT,
|
||||||
|
} from '../../subscriptions/constants/subscription.constants';
|
||||||
|
|
||||||
|
export class AddFreeTrialSubscription1722700000000 implements MigrationInterface {
|
||||||
|
name = 'AddFreeTrialSubscription1722700000000';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.addColumns('subscription_plans', [
|
||||||
|
new TableColumn({
|
||||||
|
name: 'durationDays',
|
||||||
|
type: 'int',
|
||||||
|
isNullable: true,
|
||||||
|
}),
|
||||||
|
new TableColumn({
|
||||||
|
name: 'isTrial',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await queryRunner.addColumn(
|
||||||
|
'salon_subscriptions',
|
||||||
|
new TableColumn({
|
||||||
|
name: 'expiryNotifiedAt',
|
||||||
|
type: 'timestamp',
|
||||||
|
isNullable: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`
|
||||||
|
INSERT INTO subscription_plans
|
||||||
|
(name, code, "durationMonths", "durationDays", price, "freeSmsCount", "isActive", "isTrial")
|
||||||
|
VALUES
|
||||||
|
('دوره آزمایشی رایگان', $1, 0, $2, 0, $3, true, true)
|
||||||
|
ON CONFLICT (code) DO NOTHING
|
||||||
|
`,
|
||||||
|
[FREE_TRIAL_PLAN_CODE, FREE_TRIAL_DURATION_DAYS, FREE_TRIAL_SMS_COUNT],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`DELETE FROM subscription_plans WHERE code = '${FREE_TRIAL_PLAN_CODE}'`,
|
||||||
|
);
|
||||||
|
await queryRunner.dropColumn('salon_subscriptions', 'expiryNotifiedAt');
|
||||||
|
await queryRunner.dropColumn('subscription_plans', 'isTrial');
|
||||||
|
await queryRunner.dropColumn('subscription_plans', 'durationDays');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -299,6 +299,35 @@ export const NOTIFICATION_SCENARIOS: NotificationScenarioDefinition[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: NotificationScenarioKey.SUBSCRIPTION_EXPIRED,
|
||||||
|
label: 'پایان دوره اشتراک یا آزمایشی',
|
||||||
|
description:
|
||||||
|
'اعلان برای سالن هنگامی که دوره آزمایشی رایگان یا اشتراک خریداریشده به پایان میرسد',
|
||||||
|
defaultTitle: 'دوره {planLabel} شما به پایان رسید',
|
||||||
|
defaultBody:
|
||||||
|
'{planLabel} سالن {salonName} در تاریخ {endDate} به پایان رسید. برای ادامه استفاده از امکانات و ارسال پیامک، اشتراک خود را تهیه کنید.',
|
||||||
|
variables: [
|
||||||
|
{
|
||||||
|
key: 'salonName',
|
||||||
|
label: 'نام سالن',
|
||||||
|
description: 'نام سالن زیبایی',
|
||||||
|
sampleValue: 'سالن زیبایی رز',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'planLabel',
|
||||||
|
label: 'نوع دوره',
|
||||||
|
description: 'دوره آزمایشی رایگان یا نام پلن خریداریشده',
|
||||||
|
sampleValue: 'دوره آزمایشی رایگان',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'endDate',
|
||||||
|
label: 'تاریخ پایان',
|
||||||
|
description: 'تاریخ پایان دوره',
|
||||||
|
sampleValue: '۱۴۰۴/۰۴/۲۰',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const NOTIFICATION_SCENARIO_MAP = new Map(
|
export const NOTIFICATION_SCENARIO_MAP = new Map(
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ export enum NotificationScenarioKey {
|
|||||||
CAMPAIGN_SENT = 'campaign_sent',
|
CAMPAIGN_SENT = 'campaign_sent',
|
||||||
CAMPAIGN_FAILED = 'campaign_failed',
|
CAMPAIGN_FAILED = 'campaign_failed',
|
||||||
DEPOSIT_RECEIPT_SUBMITTED = 'deposit_receipt_submitted',
|
DEPOSIT_RECEIPT_SUBMITTED = 'deposit_receipt_submitted',
|
||||||
|
SUBSCRIPTION_EXPIRED = 'subscription_expired',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,6 +158,28 @@ export class SalonNotificationsService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async notifySubscriptionExpired(params: {
|
||||||
|
salonId: string;
|
||||||
|
salonOwnerId: string;
|
||||||
|
salonName: string;
|
||||||
|
planLabel: string;
|
||||||
|
endDate: Date;
|
||||||
|
}): Promise<void> {
|
||||||
|
await this.safeCreate({
|
||||||
|
salonId: params.salonId,
|
||||||
|
userId: params.salonOwnerId,
|
||||||
|
key: NotificationScenarioKey.SUBSCRIPTION_EXPIRED,
|
||||||
|
variables: {
|
||||||
|
salonName: params.salonName,
|
||||||
|
planLabel: params.planLabel,
|
||||||
|
endDate: params.endDate.toLocaleDateString('fa-IR'),
|
||||||
|
},
|
||||||
|
link: '/subscription',
|
||||||
|
relatedEntityType: 'subscription',
|
||||||
|
relatedEntityId: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async notifyForReserve(
|
private async notifyForReserve(
|
||||||
reserveId: string,
|
reserveId: string,
|
||||||
key: NotificationScenarioKey,
|
key: NotificationScenarioKey,
|
||||||
|
|||||||
@@ -1 +1,5 @@
|
|||||||
export const SMS_ROLLOVER_GRACE_DAYS = 30;
|
export const SMS_ROLLOVER_GRACE_DAYS = 30;
|
||||||
|
|
||||||
|
export const FREE_TRIAL_PLAN_CODE = 'free-trial';
|
||||||
|
export const FREE_TRIAL_DURATION_DAYS = 7;
|
||||||
|
export const FREE_TRIAL_SMS_COUNT = 100;
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ export class SalonSubscription {
|
|||||||
@Column({ type: 'int' })
|
@Column({ type: 'int' })
|
||||||
freeSmsRemaining: number;
|
freeSmsRemaining: number;
|
||||||
|
|
||||||
|
@Column({ type: 'timestamp', nullable: true })
|
||||||
|
expiryNotifiedAt: Date | null;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ export class SubscriptionPlan {
|
|||||||
@Column({ type: 'int' })
|
@Column({ type: 'int' })
|
||||||
durationMonths: number;
|
durationMonths: number;
|
||||||
|
|
||||||
|
@Column({ type: 'int', nullable: true })
|
||||||
|
durationDays: number | null;
|
||||||
|
|
||||||
@Column({ type: 'bigint' })
|
@Column({ type: 'bigint' })
|
||||||
price: string;
|
price: string;
|
||||||
|
|
||||||
@@ -29,6 +32,9 @@ export class SubscriptionPlan {
|
|||||||
@Column({ default: true })
|
@Column({ default: true })
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
|
||||||
|
@Column({ default: false })
|
||||||
|
isTrial: boolean;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ export class SalonSubscriptionsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const plan = await this.subscriptionPlansService.findActiveById(planId);
|
const plan = await this.subscriptionPlansService.findActiveById(planId);
|
||||||
|
if (plan.isTrial) {
|
||||||
|
throw new BadRequestException('This plan cannot be purchased');
|
||||||
|
}
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const rolloverSms = await this.calculateSmsRollover(salonId, now);
|
const rolloverSms = await this.calculateSmsRollover(salonId, now);
|
||||||
const freeSmsGranted = plan.freeSmsCount + rolloverSms;
|
const freeSmsGranted = plan.freeSmsCount + rolloverSms;
|
||||||
@@ -64,6 +67,64 @@ export class SalonSubscriptionsService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grants a new salon owner a one-time 7-day free trial with a limited
|
||||||
|
* number of free SMS. Silently no-ops if the owner has ever had any
|
||||||
|
* subscription before (trial or paid), to prevent abuse via creating
|
||||||
|
* extra salons. Failures are swallowed by the caller so signup never
|
||||||
|
* fails because of trial provisioning.
|
||||||
|
*/
|
||||||
|
async startFreeTrial(
|
||||||
|
salonId: string,
|
||||||
|
ownerId: string,
|
||||||
|
): Promise<SalonSubscription | null> {
|
||||||
|
const ownerSubscriptionCount = await this.subscriptionsRepository
|
||||||
|
.createQueryBuilder('subscription')
|
||||||
|
.innerJoin('subscription.salon', 'salon')
|
||||||
|
.where('salon.ownerId = :ownerId', { ownerId })
|
||||||
|
.getCount();
|
||||||
|
|
||||||
|
if (ownerSubscriptionCount > 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const plan = await this.subscriptionPlansService.findTrialPlan();
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const subscription = this.subscriptionsRepository.create({
|
||||||
|
salonId,
|
||||||
|
planId: plan.id,
|
||||||
|
status: SalonSubscriptionStatus.ACTIVE,
|
||||||
|
startDate: now,
|
||||||
|
endDate: this.addDays(now, plan.durationDays ?? 7),
|
||||||
|
pricePaid: '0',
|
||||||
|
freeSmsGranted: plan.freeSmsCount,
|
||||||
|
freeSmsRemaining: plan.freeSmsCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.subscriptionsRepository.save(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Subscriptions whose endDate has passed but no expiry notification was sent yet. */
|
||||||
|
async findSubscriptionsPendingExpiryNotification(): Promise<
|
||||||
|
SalonSubscription[]
|
||||||
|
> {
|
||||||
|
return this.subscriptionsRepository
|
||||||
|
.createQueryBuilder('subscription')
|
||||||
|
.innerJoinAndSelect('subscription.salon', 'salon')
|
||||||
|
.innerJoinAndSelect('salon.owner', 'owner')
|
||||||
|
.innerJoinAndSelect('subscription.plan', 'plan')
|
||||||
|
.where('subscription.endDate <= :now', { now: new Date() })
|
||||||
|
.andWhere('subscription.expiryNotifiedAt IS NULL')
|
||||||
|
.getMany();
|
||||||
|
}
|
||||||
|
|
||||||
|
async markExpiryNotified(id: string): Promise<void> {
|
||||||
|
await this.subscriptionsRepository.update(id, {
|
||||||
|
expiryNotifiedAt: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async validateSubscriptionPurchase(
|
async validateSubscriptionPurchase(
|
||||||
salonId: string,
|
salonId: string,
|
||||||
planId: string,
|
planId: string,
|
||||||
@@ -74,7 +135,10 @@ export class SalonSubscriptionsService {
|
|||||||
throw new ForbiddenException();
|
throw new ForbiddenException();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.subscriptionPlansService.findActiveById(planId);
|
const plan = await this.subscriptionPlansService.findActiveById(planId);
|
||||||
|
if (plan.isTrial) {
|
||||||
|
throw new BadRequestException('This plan cannot be purchased');
|
||||||
|
}
|
||||||
|
|
||||||
const activeSubscription = await this.findActiveSubscription(salonId);
|
const activeSubscription = await this.findActiveSubscription(salonId);
|
||||||
if (activeSubscription) {
|
if (activeSubscription) {
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||||
|
import { SalonNotificationsService } from '../notifications/salon-notifications.service';
|
||||||
|
import { SmsService } from '../sms/sms.service';
|
||||||
|
import { SalonSubscriptionsService } from './salon-subscriptions.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SubscriptionAutomationService {
|
||||||
|
private readonly logger = new Logger(SubscriptionAutomationService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly salonSubscriptionsService: SalonSubscriptionsService,
|
||||||
|
private readonly salonNotificationsService: SalonNotificationsService,
|
||||||
|
private readonly smsService: SmsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects subscriptions (trial or paid) whose validity period just ended
|
||||||
|
* and, once per subscription, notifies the salon owner in-app and by SMS
|
||||||
|
* that they need to purchase a plan to keep using the product. Access is
|
||||||
|
* already blocked automatically by `ActiveSubscriptionGuard` once the
|
||||||
|
* subscription's `endDate` has passed, independent of this job.
|
||||||
|
*/
|
||||||
|
@Cron(CronExpression.EVERY_DAY_AT_10AM, {
|
||||||
|
name: 'subscription-expiry-notifications',
|
||||||
|
})
|
||||||
|
async notifyExpiredSubscriptions(): Promise<number> {
|
||||||
|
const subscriptions =
|
||||||
|
await this.salonSubscriptionsService.findSubscriptionsPendingExpiryNotification();
|
||||||
|
|
||||||
|
let notifiedCount = 0;
|
||||||
|
|
||||||
|
for (const subscription of subscriptions) {
|
||||||
|
try {
|
||||||
|
await this.salonSubscriptionsService.markExpiryNotified(
|
||||||
|
subscription.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
const planLabel = subscription.plan?.isTrial
|
||||||
|
? 'دوره آزمایشی رایگان'
|
||||||
|
: `اشتراک ${subscription.plan?.name ?? ''}`.trim();
|
||||||
|
|
||||||
|
await this.salonNotificationsService.notifySubscriptionExpired({
|
||||||
|
salonId: subscription.salonId,
|
||||||
|
salonOwnerId: subscription.salon.ownerId,
|
||||||
|
salonName: subscription.salon.name,
|
||||||
|
planLabel,
|
||||||
|
endDate: subscription.endDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ownerMobile = subscription.salon.owner?.mobile;
|
||||||
|
if (ownerMobile) {
|
||||||
|
await this.smsService.sendSms(
|
||||||
|
ownerMobile,
|
||||||
|
`${planLabel} سالن ${subscription.salon.name} به پایان رسید. برای ادامه استفاده از امکانات و ارسال پیامک، همین حالا اشتراک تهیه کنید.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
notifiedCount += 1;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`Failed to send expiry notification for subscription ${subscription.id}`,
|
||||||
|
error instanceof Error ? error.stack : undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notifiedCount > 0) {
|
||||||
|
this.logger.log(
|
||||||
|
`Sent expiry notifications for ${notifiedCount} subscription(s)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return notifiedCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,11 +22,21 @@ export class SubscriptionPlansService {
|
|||||||
|
|
||||||
findAll(activeOnly = false): Promise<SubscriptionPlan[]> {
|
findAll(activeOnly = false): Promise<SubscriptionPlan[]> {
|
||||||
return this.plansRepository.find({
|
return this.plansRepository.find({
|
||||||
where: activeOnly ? { isActive: true } : undefined,
|
where: { isActive: activeOnly ? true : undefined, isTrial: false },
|
||||||
order: { durationMonths: 'ASC' },
|
order: { durationMonths: 'ASC' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findTrialPlan(): Promise<SubscriptionPlan> {
|
||||||
|
const plan = await this.plansRepository.findOne({
|
||||||
|
where: { isTrial: true, isActive: true },
|
||||||
|
});
|
||||||
|
if (!plan) {
|
||||||
|
throw new NotFoundException('Free trial plan is not configured');
|
||||||
|
}
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
async findOne(id: string): Promise<SubscriptionPlan> {
|
async findOne(id: string): Promise<SubscriptionPlan> {
|
||||||
const plan = await this.plansRepository.findOne({ where: { id } });
|
const plan = await this.plansRepository.findOne({ where: { id } });
|
||||||
if (!plan) {
|
if (!plan) {
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import { APP_GUARD } from '@nestjs/core';
|
|||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AuthGuardsModule } from '../auth/auth-guards.module';
|
import { AuthGuardsModule } from '../auth/auth-guards.module';
|
||||||
import { AuthorizationModule } from '../authorization/authorization.module';
|
import { AuthorizationModule } from '../authorization/authorization.module';
|
||||||
|
import { NotificationsModule } from '../notifications/notifications.module';
|
||||||
import { PaymentsModule } from '../payments/payments.module';
|
import { PaymentsModule } from '../payments/payments.module';
|
||||||
import { SalonsModule } from '../salons/salons.module';
|
import { SalonsModule } from '../salons/salons.module';
|
||||||
|
import { SmsModule } from '../sms/sms.module';
|
||||||
import { SalonSmsPurchase } from './entities/salon-sms-purchase.entity';
|
import { SalonSmsPurchase } from './entities/salon-sms-purchase.entity';
|
||||||
import { SalonSubscription } from './entities/salon-subscription.entity';
|
import { SalonSubscription } from './entities/salon-subscription.entity';
|
||||||
import { SmsPackage } from './entities/sms-package.entity';
|
import { SmsPackage } from './entities/sms-package.entity';
|
||||||
@@ -14,6 +16,7 @@ import { SalonSubscriptionsController } from './salon-subscriptions.controller';
|
|||||||
import { SalonSubscriptionsService } from './salon-subscriptions.service';
|
import { SalonSubscriptionsService } from './salon-subscriptions.service';
|
||||||
import { SmsPackagesController } from './sms-packages.controller';
|
import { SmsPackagesController } from './sms-packages.controller';
|
||||||
import { SmsPackagesService } from './sms-packages.service';
|
import { SmsPackagesService } from './sms-packages.service';
|
||||||
|
import { SubscriptionAutomationService } from './subscription-automation.service';
|
||||||
import { SubscriptionPlansController } from './subscription-plans.controller';
|
import { SubscriptionPlansController } from './subscription-plans.controller';
|
||||||
import { SubscriptionPlansService } from './subscription-plans.service';
|
import { SubscriptionPlansService } from './subscription-plans.service';
|
||||||
|
|
||||||
@@ -28,6 +31,8 @@ import { SubscriptionPlansService } from './subscription-plans.service';
|
|||||||
SalonsModule,
|
SalonsModule,
|
||||||
AuthorizationModule,
|
AuthorizationModule,
|
||||||
AuthGuardsModule,
|
AuthGuardsModule,
|
||||||
|
NotificationsModule,
|
||||||
|
SmsModule,
|
||||||
forwardRef(() => PaymentsModule),
|
forwardRef(() => PaymentsModule),
|
||||||
],
|
],
|
||||||
controllers: [
|
controllers: [
|
||||||
@@ -39,6 +44,7 @@ import { SubscriptionPlansService } from './subscription-plans.service';
|
|||||||
SubscriptionPlansService,
|
SubscriptionPlansService,
|
||||||
SmsPackagesService,
|
SmsPackagesService,
|
||||||
SalonSubscriptionsService,
|
SalonSubscriptionsService,
|
||||||
|
SubscriptionAutomationService,
|
||||||
{
|
{
|
||||||
provide: APP_GUARD,
|
provide: APP_GUARD,
|
||||||
useClass: ActiveSubscriptionGuard,
|
useClass: ActiveSubscriptionGuard,
|
||||||
|
|||||||
Reference in New Issue
Block a user