🚀 Initialize Musadaq SaaS: Full Backend + AI + React Dashboard + Docker Setup
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* ════════════════════════════════════════════════════════════
|
||||
* مُصادَق (Musadaq) — Subscription Entity
|
||||
* ════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Tenant } from '../../tenants/entities/tenant.entity';
|
||||
|
||||
export enum SubscriptionPlan {
|
||||
BASIC = 'basic',
|
||||
OFFICE = 'office',
|
||||
PRO = 'pro',
|
||||
ENTERPRISE = 'enterprise',
|
||||
}
|
||||
|
||||
export enum SubscriptionStatus {
|
||||
ACTIVE = 'active',
|
||||
PAST_DUE = 'past_due',
|
||||
CANCELLED = 'cancelled',
|
||||
}
|
||||
|
||||
@Entity('subscriptions')
|
||||
export class Subscription {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||
tenant_id!: string;
|
||||
|
||||
@ManyToOne(() => Tenant, (tenant) => tenant.subscriptions, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'tenant_id' })
|
||||
tenant!: Tenant;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: SubscriptionPlan,
|
||||
})
|
||||
plan!: SubscriptionPlan;
|
||||
|
||||
@Column({ type: 'int' })
|
||||
max_companies!: number;
|
||||
|
||||
@Column({ type: 'int' })
|
||||
max_invoices_per_month!: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
||||
price_jod!: number;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: ['monthly', 'annual'],
|
||||
default: 'monthly',
|
||||
})
|
||||
billing_cycle!: string;
|
||||
|
||||
@Column({ type: 'timestamp', nullable: true })
|
||||
current_period_start?: Date;
|
||||
|
||||
@Column({ type: 'timestamp', nullable: true })
|
||||
current_period_end?: Date;
|
||||
|
||||
@Column({ type: 'int', default: 0 })
|
||||
invoices_used_this_month!: number;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: SubscriptionStatus,
|
||||
default: SubscriptionStatus.ACTIVE,
|
||||
})
|
||||
status!: SubscriptionStatus;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamp' })
|
||||
created_at!: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamp' })
|
||||
updated_at!: Date;
|
||||
}
|
||||
21
backend/src/modules/subscriptions/subscription.module.ts
Normal file
21
backend/src/modules/subscriptions/subscription.module.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* ════════════════════════════════════════════════════════════
|
||||
* مُصادَق (Musadaq) — Subscriptions Module
|
||||
* ════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { SubscriptionsService } from './subscription.service';
|
||||
import { Subscription } from './entities/subscription.entity';
|
||||
import { CompaniesModule } from '../companies/company.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Subscription]),
|
||||
forwardRef(() => CompaniesModule),
|
||||
],
|
||||
providers: [SubscriptionsService],
|
||||
exports: [SubscriptionsService],
|
||||
})
|
||||
export class SubscriptionsModule {}
|
||||
64
backend/src/modules/subscriptions/subscription.service.ts
Normal file
64
backend/src/modules/subscriptions/subscription.service.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* ════════════════════════════════════════════════════════════
|
||||
* مُصادَق (Musadaq) — Subscriptions Service
|
||||
* ════════════════════════════════════════════════════════════
|
||||
* يدير خطص الاشتراك والحدود المسموح بها للمكاتب.
|
||||
* ════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Subscription, SubscriptionStatus } from './entities/subscription.entity';
|
||||
|
||||
@Injectable()
|
||||
export class SubscriptionsService {
|
||||
constructor(
|
||||
@InjectRepository(Subscription)
|
||||
private subscriptionRepository: Repository<Subscription>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* الحصول على اشتراك المكتب الحالي
|
||||
*/
|
||||
async findActive(tenantId: string): Promise<Subscription> {
|
||||
const subscription = await this.subscriptionRepository.findOne({
|
||||
where: { tenant_id: tenantId, status: SubscriptionStatus.ACTIVE },
|
||||
});
|
||||
if (!subscription) throw new NotFoundException('Active subscription not found');
|
||||
return subscription;
|
||||
}
|
||||
|
||||
/**
|
||||
* هل يُسمح بإضافة شركة جديدة؟
|
||||
*/
|
||||
async checkCompanyLimit(tenantId: string): Promise<boolean> {
|
||||
const sub = await this.findActive(tenantId);
|
||||
|
||||
// Check current company count
|
||||
const count = await this.subscriptionRepository.manager.count('companies', {
|
||||
where: { tenant_id: tenantId, is_active: true },
|
||||
});
|
||||
|
||||
return count < sub.max_companies || sub.max_companies === -1; // -1 for unlimited
|
||||
}
|
||||
|
||||
/**
|
||||
* هل يُسمح برفع فاتورة جديدة؟
|
||||
*/
|
||||
async checkInvoiceLimit(tenantId: string): Promise<boolean> {
|
||||
const sub = await this.findActive(tenantId);
|
||||
|
||||
return sub.invoices_used_this_month < sub.max_invoices_per_month || sub.max_invoices_per_month === -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* زيادة عداد الفواتير بعد الرفع الناجح
|
||||
*/
|
||||
async incrementInvoiceCount(tenantId: string): Promise<void> {
|
||||
const sub = await this.findActive(tenantId);
|
||||
await this.subscriptionRepository.update(sub.id, {
|
||||
invoices_used_this_month: sub.invoices_used_this_month + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user