🚀 Initialize Musadaq SaaS: Full Backend + AI + React Dashboard + Docker Setup

This commit is contained in:
Hamza-Ayed
2026-04-16 23:26:32 +03:00
commit d66891ba0f
221 changed files with 13079 additions and 0 deletions

View File

@@ -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;
}

View 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 {}

View 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,
});
}
}