🚀 Feature: Implement Global Super Admin access & bypass tenant filtering
This commit is contained in:
@@ -27,43 +27,43 @@ export class CompaniesController {
|
|||||||
constructor(private companiesService: CompaniesService) {}
|
constructor(private companiesService: CompaniesService) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@Roles(UserRole.ADMIN, UserRole.ACCOUNTANT)
|
@Roles(UserRole.ADMIN, UserRole.ACCOUNTANT, UserRole.SUPER_ADMIN)
|
||||||
async create(@CurrentUser() user: any, @Body() dto: any) {
|
async create(@CurrentUser() user: any, @Body() dto: any) {
|
||||||
return this.companiesService.create(user.tenantId, dto);
|
return this.companiesService.create(user, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
async findAll(@CurrentUser() user: any) {
|
async findAll(@CurrentUser() user: any) {
|
||||||
return this.companiesService.findAll(user.tenantId);
|
return this.companiesService.findAll(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
async findOne(@CurrentUser() user: any, @Param('id') id: string) {
|
async findOne(@CurrentUser() user: any, @Param('id') id: string) {
|
||||||
return this.companiesService.findOne(user.tenantId, id);
|
return this.companiesService.findOne(user, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
@Roles(UserRole.ADMIN, UserRole.ACCOUNTANT)
|
@Roles(UserRole.ADMIN, UserRole.ACCOUNTANT, UserRole.SUPER_ADMIN)
|
||||||
async update(
|
async update(
|
||||||
@CurrentUser() user: any,
|
@CurrentUser() user: any,
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
@Body() dto: any,
|
@Body() dto: any,
|
||||||
) {
|
) {
|
||||||
return this.companiesService.update(user.tenantId, id, dto);
|
return this.companiesService.update(user, id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ربط بيانات جو فوترة المشفرة
|
* ربط بيانات جو فوترة المشفرة
|
||||||
*/
|
*/
|
||||||
@Put(':id/jofotara')
|
@Put(':id/jofotara')
|
||||||
@Roles(UserRole.ADMIN)
|
@Roles(UserRole.ADMIN, UserRole.SUPER_ADMIN)
|
||||||
async setJoFotara(
|
async setJoFotara(
|
||||||
@CurrentUser() user: any,
|
@CurrentUser() user: any,
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
@Body() dto: { clientId: string; secretKey: string },
|
@Body() dto: { clientId: string; secretKey: string },
|
||||||
) {
|
) {
|
||||||
return this.companiesService.setJoFotaraCredentials(
|
return this.companiesService.setJoFotaraCredentials(
|
||||||
user.tenantId,
|
user,
|
||||||
id,
|
id,
|
||||||
dto.clientId,
|
dto.clientId,
|
||||||
dto.secretKey,
|
dto.secretKey,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { Company } from './entities/company.entity';
|
import { Company } from './entities/company.entity';
|
||||||
|
import { UserRole } from '../users/enums/role.enum';
|
||||||
import { EncryptionService } from '../../services/encryption/encryption.service';
|
import { EncryptionService } from '../../services/encryption/encryption.service';
|
||||||
import { SubscriptionsService } from '../subscriptions/subscription.service';
|
import { SubscriptionsService } from '../subscriptions/subscription.service';
|
||||||
|
|
||||||
@@ -30,7 +31,8 @@ export class CompaniesService {
|
|||||||
/**
|
/**
|
||||||
* إنشاء شركة جديدة مع التحقق من حدود الاشتراك
|
* إنشاء شركة جديدة مع التحقق من حدود الاشتراك
|
||||||
*/
|
*/
|
||||||
async create(tenantId: string, dto: any): Promise<Company> {
|
async create(user: any, dto: any): Promise<Company> {
|
||||||
|
const tenantId = user.tenantId;
|
||||||
// 1. Check subscription limits
|
// 1. Check subscription limits
|
||||||
const canCreate = await this.subscriptionsService.checkCompanyLimit(tenantId);
|
const canCreate = await this.subscriptionsService.checkCompanyLimit(tenantId);
|
||||||
if (!canCreate) {
|
if (!canCreate) {
|
||||||
@@ -48,9 +50,12 @@ export class CompaniesService {
|
|||||||
/**
|
/**
|
||||||
* قائمة الشركات التابعة للمكتب
|
* قائمة الشركات التابعة للمكتب
|
||||||
*/
|
*/
|
||||||
async findAll(tenantId: string): Promise<Company[]> {
|
async findAll(user: any): Promise<Company[]> {
|
||||||
|
const isSuperAdmin = user.role === UserRole.SUPER_ADMIN;
|
||||||
|
const filter = isSuperAdmin ? { is_active: true } : { tenant_id: user.tenantId, is_active: true };
|
||||||
|
|
||||||
return this.companyRepository.find({
|
return this.companyRepository.find({
|
||||||
where: { tenant_id: tenantId, is_active: true },
|
where: filter,
|
||||||
order: { created_at: 'DESC' },
|
order: { created_at: 'DESC' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -58,9 +63,12 @@ export class CompaniesService {
|
|||||||
/**
|
/**
|
||||||
* تفاصيل شركة محددة
|
* تفاصيل شركة محددة
|
||||||
*/
|
*/
|
||||||
async findOne(tenantId: string, id: string): Promise<Company> {
|
async findOne(user: any, id: string): Promise<Company> {
|
||||||
|
const isSuperAdmin = user.role === UserRole.SUPER_ADMIN;
|
||||||
|
const filter = isSuperAdmin ? { id } : { id, tenant_id: user.tenantId };
|
||||||
|
|
||||||
const company = await this.companyRepository.findOne({
|
const company = await this.companyRepository.findOne({
|
||||||
where: { id, tenant_id: tenantId },
|
where: filter,
|
||||||
});
|
});
|
||||||
if (!company) throw new NotFoundException('Company not found');
|
if (!company) throw new NotFoundException('Company not found');
|
||||||
return company;
|
return company;
|
||||||
@@ -69,8 +77,8 @@ export class CompaniesService {
|
|||||||
/**
|
/**
|
||||||
* تحديث بيانات شركة
|
* تحديث بيانات شركة
|
||||||
*/
|
*/
|
||||||
async update(tenantId: string, id: string, dto: any): Promise<Company> {
|
async update(user: any, id: string, dto: any): Promise<Company> {
|
||||||
const company = await this.findOne(tenantId, id);
|
const company = await this.findOne(user, id);
|
||||||
Object.assign(company, dto);
|
Object.assign(company, dto);
|
||||||
return this.companyRepository.save(company);
|
return this.companyRepository.save(company);
|
||||||
}
|
}
|
||||||
@@ -79,12 +87,12 @@ export class CompaniesService {
|
|||||||
* حفظ مفاتيح جو فوترة (مُشفرة)
|
* حفظ مفاتيح جو فوترة (مُشفرة)
|
||||||
*/
|
*/
|
||||||
async setJoFotaraCredentials(
|
async setJoFotaraCredentials(
|
||||||
tenantId: string,
|
user: any,
|
||||||
id: string,
|
id: string,
|
||||||
clientId: string,
|
clientId: string,
|
||||||
secretKey: string,
|
secretKey: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const company = await this.findOne(tenantId, id);
|
const company = await this.findOne(user, id);
|
||||||
|
|
||||||
company.jofotara_client_id_encrypted = this.encryptionService.encrypt(clientId);
|
company.jofotara_client_id_encrypted = this.encryptionService.encrypt(clientId);
|
||||||
company.jofotara_secret_key_encrypted = this.encryptionService.encrypt(secretKey);
|
company.jofotara_secret_key_encrypted = this.encryptionService.encrypt(secretKey);
|
||||||
@@ -95,9 +103,12 @@ export class CompaniesService {
|
|||||||
/**
|
/**
|
||||||
* الحصول على المفاتيح (مفكوك تشفيرها) — للاستخدام الداخلي فقط
|
* الحصول على المفاتيح (مفكوك تشفيرها) — للاستخدام الداخلي فقط
|
||||||
*/
|
*/
|
||||||
async getDecryptedCredentials(tenantId: string, id: string) {
|
async getDecryptedCredentials(user: any, id: string) {
|
||||||
|
const isSuperAdmin = user.role === UserRole.SUPER_ADMIN;
|
||||||
|
const filter = isSuperAdmin ? { id } : { id, tenant_id: user.tenantId };
|
||||||
|
|
||||||
const company = await this.companyRepository.findOne({
|
const company = await this.companyRepository.findOne({
|
||||||
where: { id, tenant_id: tenantId },
|
where: filter,
|
||||||
select: ['jofotara_client_id_encrypted', 'jofotara_secret_key_encrypted'],
|
select: ['jofotara_client_id_encrypted', 'jofotara_secret_key_encrypted'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,16 +10,16 @@ export class DashboardController {
|
|||||||
|
|
||||||
@Get('stats')
|
@Get('stats')
|
||||||
async getStats(@CurrentUser() user: any) {
|
async getStats(@CurrentUser() user: any) {
|
||||||
return this.dashboardService.getStats(user.tenantId);
|
return this.dashboardService.getStats(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('multi-entity')
|
@Get('multi-entity')
|
||||||
async getMultiEntityStats(@CurrentUser() user: any) {
|
async getMultiEntityStats(@CurrentUser() user: any) {
|
||||||
return this.dashboardService.getMultiEntityStats(user.tenantId);
|
return this.dashboardService.getMultiEntityStats(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('risk-invoices')
|
@Get('risk-invoices')
|
||||||
async getRiskInvoices(@CurrentUser() user: any) {
|
async getRiskInvoices(@CurrentUser() user: any) {
|
||||||
return this.dashboardService.getRiskInvoices(user.tenantId);
|
return this.dashboardService.getRiskInvoices(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import { Repository, Between } from 'typeorm';
|
import { Repository, Between } from 'typeorm';
|
||||||
import { Invoice, InvoiceStatus } from '../invoices/entities/invoice.entity';
|
import { Invoice, InvoiceStatus } from '../invoices/entities/invoice.entity';
|
||||||
import { Company } from '../companies/entities/company.entity';
|
import { Company } from '../companies/entities/company.entity';
|
||||||
|
import { UserRole } from '../users/enums/role.enum';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DashboardService {
|
export class DashboardService {
|
||||||
@@ -13,23 +14,29 @@ export class DashboardService {
|
|||||||
private companyRepository: Repository<Company>,
|
private companyRepository: Repository<Company>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getStats(tenantId: string) {
|
async getStats(user: any) {
|
||||||
|
const isSuperAdmin = user.role === UserRole.SUPER_ADMIN;
|
||||||
|
const filter = isSuperAdmin ? {} : { tenant_id: user.tenantId };
|
||||||
|
|
||||||
const totalInvoices = await this.invoiceRepository.count({
|
const totalInvoices = await this.invoiceRepository.count({
|
||||||
where: { tenant_id: tenantId },
|
where: filter,
|
||||||
});
|
});
|
||||||
|
|
||||||
const approvedInvoices = await this.invoiceRepository.count({
|
const approvedInvoices = await this.invoiceRepository.count({
|
||||||
where: { tenant_id: tenantId, status: InvoiceStatus.APPROVED },
|
where: { ...filter, status: InvoiceStatus.APPROVED },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Using QueryBuilder for better control
|
// Using QueryBuilder for better control
|
||||||
const statuses = await this.invoiceRepository
|
const query = this.invoiceRepository
|
||||||
.createQueryBuilder('invoice')
|
.createQueryBuilder('invoice')
|
||||||
.select('status')
|
.select('status')
|
||||||
.addSelect('COUNT(*)', 'count')
|
.addSelect('COUNT(*)', 'count');
|
||||||
.where('invoice.tenant_id = :tenantId', { tenantId })
|
|
||||||
.groupBy('status')
|
if (!isSuperAdmin) {
|
||||||
.getRawMany();
|
query.where('invoice.tenant_id = :tenantId', { tenantId: user.tenantId });
|
||||||
|
}
|
||||||
|
|
||||||
|
const statuses = await query.groupBy('status').getRawMany();
|
||||||
|
|
||||||
const statusMap = statuses.reduce((acc, curr) => {
|
const statusMap = statuses.reduce((acc, curr) => {
|
||||||
acc[curr.status] = parseInt(curr.count);
|
acc[curr.status] = parseInt(curr.count);
|
||||||
@@ -37,12 +44,12 @@ export class DashboardService {
|
|||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const companiesCount = await this.companyRepository.count({
|
const companiesCount = await this.companyRepository.count({
|
||||||
where: { tenant_id: tenantId },
|
where: filter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calculate total tax (mock logic for now, should sum up tax fields)
|
// Calculate total tax (mock logic for now, should sum up tax fields)
|
||||||
const invoices = await this.invoiceRepository.find({
|
const invoices = await this.invoiceRepository.find({
|
||||||
where: { tenant_id: tenantId, status: InvoiceStatus.APPROVED },
|
where: { ...filter, status: InvoiceStatus.APPROVED },
|
||||||
select: ['tax_amount'],
|
select: ['tax_amount'],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -50,7 +57,7 @@ export class DashboardService {
|
|||||||
|
|
||||||
// Get recent activities (last 5 invoices)
|
// Get recent activities (last 5 invoices)
|
||||||
const recentInvoices = await this.invoiceRepository.find({
|
const recentInvoices = await this.invoiceRepository.find({
|
||||||
where: { tenant_id: tenantId },
|
where: filter,
|
||||||
order: { created_at: 'DESC' },
|
order: { created_at: 'DESC' },
|
||||||
take: 5,
|
take: 5,
|
||||||
relations: ['company'],
|
relations: ['company'],
|
||||||
@@ -81,9 +88,12 @@ export class DashboardService {
|
|||||||
* جلب إحصائيات مجمعة لكل الشركات (Elite Accountant View)
|
* جلب إحصائيات مجمعة لكل الشركات (Elite Accountant View)
|
||||||
* Get summarized stats for all companies under a tenant
|
* Get summarized stats for all companies under a tenant
|
||||||
*/
|
*/
|
||||||
async getMultiEntityStats(tenantId: string) {
|
async getMultiEntityStats(user: any) {
|
||||||
|
const isSuperAdmin = user.role === UserRole.SUPER_ADMIN;
|
||||||
|
const filter = isSuperAdmin ? {} : { tenant_id: user.tenantId };
|
||||||
|
|
||||||
const companies = await this.companyRepository.find({
|
const companies = await this.companyRepository.find({
|
||||||
where: { tenant_id: tenantId },
|
where: filter,
|
||||||
});
|
});
|
||||||
|
|
||||||
const companyStats = await Promise.all(
|
const companyStats = await Promise.all(
|
||||||
@@ -133,11 +143,14 @@ export class DashboardService {
|
|||||||
/**
|
/**
|
||||||
* جلب الفواتير التي بها مخاطر (فشل التحقق أو مرفوضة) عبر كافة الشركات
|
* جلب الفواتير التي بها مخاطر (فشل التحقق أو مرفوضة) عبر كافة الشركات
|
||||||
*/
|
*/
|
||||||
async getRiskInvoices(tenantId: string) {
|
async getRiskInvoices(user: any) {
|
||||||
|
const isSuperAdmin = user.role === UserRole.SUPER_ADMIN;
|
||||||
|
const filter = isSuperAdmin ? {} : { tenant_id: user.tenantId };
|
||||||
|
|
||||||
return this.invoiceRepository.find({
|
return this.invoiceRepository.find({
|
||||||
where: [
|
where: [
|
||||||
{ tenant_id: tenantId, status: InvoiceStatus.VALIDATION_FAILED },
|
{ ...filter, status: InvoiceStatus.VALIDATION_FAILED },
|
||||||
{ tenant_id: tenantId, status: InvoiceStatus.REJECTED },
|
{ ...filter, status: InvoiceStatus.REJECTED },
|
||||||
],
|
],
|
||||||
relations: ['company'],
|
relations: ['company'],
|
||||||
order: { updated_at: 'DESC' },
|
order: { updated_at: 'DESC' },
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export class InvoicesController {
|
|||||||
*/
|
*/
|
||||||
@Get()
|
@Get()
|
||||||
async findAllByTenant(@CurrentUser() user: any) {
|
async findAllByTenant(@CurrentUser() user: any) {
|
||||||
return this.invoicesService.findAllByTenant(user.tenantId);
|
return this.invoicesService.findAllByTenant(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,14 +48,14 @@ export class InvoicesController {
|
|||||||
* Upload an invoice file for a specific company
|
* Upload an invoice file for a specific company
|
||||||
*/
|
*/
|
||||||
@Post('upload/:companyId')
|
@Post('upload/:companyId')
|
||||||
@Roles(UserRole.ADMIN, UserRole.ACCOUNTANT)
|
@Roles(UserRole.ADMIN, UserRole.ACCOUNTANT, UserRole.SUPER_ADMIN)
|
||||||
@UseInterceptors(FileInterceptor('file'))
|
@UseInterceptors(FileInterceptor('file'))
|
||||||
async upload(
|
async upload(
|
||||||
@CurrentUser() user: any,
|
@CurrentUser() user: any,
|
||||||
@Param('companyId', ParseUUIDPipe) companyId: string,
|
@Param('companyId', ParseUUIDPipe) companyId: string,
|
||||||
@UploadedFile() file: Express.Multer.File,
|
@UploadedFile() file: Express.Multer.File,
|
||||||
) {
|
) {
|
||||||
return this.invoicesService.upload(user.tenantId, companyId, file);
|
return this.invoicesService.upload(user, companyId, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,7 +67,7 @@ export class InvoicesController {
|
|||||||
@CurrentUser() user: any,
|
@CurrentUser() user: any,
|
||||||
@Param('companyId', ParseUUIDPipe) companyId: string,
|
@Param('companyId', ParseUUIDPipe) companyId: string,
|
||||||
) {
|
) {
|
||||||
return this.invoicesService.findAll(user.tenantId, companyId);
|
return this.invoicesService.findAll(user, companyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -79,7 +79,7 @@ export class InvoicesController {
|
|||||||
@CurrentUser() user: any,
|
@CurrentUser() user: any,
|
||||||
@Param('id', ParseUUIDPipe) id: string,
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
) {
|
) {
|
||||||
return this.invoicesService.findOne(user.tenantId, id);
|
return this.invoicesService.findOne(user, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -90,9 +90,9 @@ export class InvoicesController {
|
|||||||
* Submit an invoice to the official JoFotara portal
|
* Submit an invoice to the official JoFotara portal
|
||||||
*/
|
*/
|
||||||
@Post(':id/submit')
|
@Post(':id/submit')
|
||||||
@Roles(UserRole.ADMIN, UserRole.ACCOUNTANT)
|
@Roles(UserRole.ADMIN, UserRole.ACCOUNTANT, UserRole.SUPER_ADMIN)
|
||||||
async submit(@CurrentUser() user: any, @Param('id', ParseUUIDPipe) id: string) {
|
async submit(@CurrentUser() user: any, @Param('id', ParseUUIDPipe) id: string) {
|
||||||
return this.invoicesService.submitToJoFotara(user.tenantId, id);
|
return this.invoicesService.submitToJoFotara(user, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -100,9 +100,9 @@ export class InvoicesController {
|
|||||||
* Permanently delete an invoice
|
* Permanently delete an invoice
|
||||||
*/
|
*/
|
||||||
@Post(':id/delete') // Using POST for delete to match frontend request style or use standard DELETE
|
@Post(':id/delete') // Using POST for delete to match frontend request style or use standard DELETE
|
||||||
@Roles(UserRole.ADMIN, UserRole.ACCOUNTANT)
|
@Roles(UserRole.ADMIN, UserRole.ACCOUNTANT, UserRole.SUPER_ADMIN)
|
||||||
async remove(@CurrentUser() user: any, @Param('id', ParseUUIDPipe) id: string) {
|
async remove(@CurrentUser() user: any, @Param('id', ParseUUIDPipe) id: string) {
|
||||||
return this.invoicesService.remove(user.tenantId, id);
|
return this.invoicesService.remove(user, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -115,11 +115,11 @@ export class InvoicesController {
|
|||||||
@Param('id', ParseUUIDPipe) id: string,
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
@Res({ passthrough: true }) res: Response,
|
@Res({ passthrough: true }) res: Response,
|
||||||
) {
|
) {
|
||||||
const streamableFile = await this.invoicesService.getFile(user.tenantId, id);
|
const streamableFile = await this.invoicesService.getFile(user, id);
|
||||||
|
|
||||||
// We need to determine the content type to ensure it opens inline in the browser (iframe)
|
// We need to determine the content type to ensure it opens inline in the browser (iframe)
|
||||||
// We can fetch the invoice details first to get the extension.
|
// We can fetch the invoice details first to get the extension.
|
||||||
const invoice = await this.invoicesService.findOne(user.tenantId, id);
|
const invoice = await this.invoicesService.findOne(user, id);
|
||||||
let mimeType = 'application/pdf'; // Default fallback
|
let mimeType = 'application/pdf'; // Default fallback
|
||||||
if (invoice.original_file_path) {
|
if (invoice.original_file_path) {
|
||||||
const ext = invoice.original_file_path.split('.').pop()?.toLowerCase();
|
const ext = invoice.original_file_path.split('.').pop()?.toLowerCase();
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import * as path from 'path';
|
|||||||
import { InjectQueue } from '@nestjs/bull';
|
import { InjectQueue } from '@nestjs/bull';
|
||||||
import { Queue } from 'bull';
|
import { Queue } from 'bull';
|
||||||
import { Invoice, InvoiceStatus } from './entities/invoice.entity';
|
import { Invoice, InvoiceStatus } from './entities/invoice.entity';
|
||||||
|
import { UserRole } from '../users/enums/role.enum';
|
||||||
import { LocalStorageService } from '../../services/storage/local-storage.service';
|
import { LocalStorageService } from '../../services/storage/local-storage.service';
|
||||||
import { SubscriptionsService } from '../subscriptions/subscription.service';
|
import { SubscriptionsService } from '../subscriptions/subscription.service';
|
||||||
import { UBLGeneratorService } from './ubl-generator.service';
|
import { UBLGeneratorService } from './ubl-generator.service';
|
||||||
@@ -52,10 +53,11 @@ export class InvoicesService {
|
|||||||
* Upload a new invoice and initiate extraction/validation
|
* Upload a new invoice and initiate extraction/validation
|
||||||
*/
|
*/
|
||||||
async upload(
|
async upload(
|
||||||
tenantId: string,
|
user: any,
|
||||||
companyId: string,
|
companyId: string,
|
||||||
file: Express.Multer.File,
|
file: Express.Multer.File,
|
||||||
): Promise<Invoice> {
|
): Promise<Invoice> {
|
||||||
|
const tenantId = user.tenantId;
|
||||||
const canUpload = await this.subscriptionsService.checkInvoiceLimit(tenantId);
|
const canUpload = await this.subscriptionsService.checkInvoiceLimit(tenantId);
|
||||||
if (!canUpload) {
|
if (!canUpload) {
|
||||||
throw new ForbiddenException('Monthly invoice limit reached for your current plan');
|
throw new ForbiddenException('Monthly invoice limit reached for your current plan');
|
||||||
@@ -95,9 +97,12 @@ export class InvoicesService {
|
|||||||
* قائمة الفواتير لشركة محددة
|
* قائمة الفواتير لشركة محددة
|
||||||
* Find all invoices for a specific company
|
* Find all invoices for a specific company
|
||||||
*/
|
*/
|
||||||
async findAll(tenantId: string, companyId: string): Promise<Invoice[]> {
|
async findAll(user: any, companyId: string): Promise<Invoice[]> {
|
||||||
|
const isSuperAdmin = user.role === UserRole.SUPER_ADMIN;
|
||||||
|
const filter = isSuperAdmin ? { company_id: companyId } : { tenant_id: user.tenantId, company_id: companyId };
|
||||||
|
|
||||||
return this.invoiceRepository.find({
|
return this.invoiceRepository.find({
|
||||||
where: { tenant_id: tenantId, company_id: companyId },
|
where: filter,
|
||||||
order: { created_at: 'DESC' },
|
order: { created_at: 'DESC' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -106,9 +111,12 @@ export class InvoicesService {
|
|||||||
* قائمة جميع الفواتير للمكتب (المستأجر)
|
* قائمة جميع الفواتير للمكتب (المستأجر)
|
||||||
* Find all invoices for the entire tenant (accounting office)
|
* Find all invoices for the entire tenant (accounting office)
|
||||||
*/
|
*/
|
||||||
async findAllByTenant(tenantId: string): Promise<Invoice[]> {
|
async findAllByTenant(user: any): Promise<Invoice[]> {
|
||||||
|
const isSuperAdmin = user.role === UserRole.SUPER_ADMIN;
|
||||||
|
const filter = isSuperAdmin ? {} : { tenant_id: user.tenantId };
|
||||||
|
|
||||||
return this.invoiceRepository.find({
|
return this.invoiceRepository.find({
|
||||||
where: { tenant_id: tenantId },
|
where: filter,
|
||||||
relations: ['company'],
|
relations: ['company'],
|
||||||
order: { created_at: 'DESC' },
|
order: { created_at: 'DESC' },
|
||||||
});
|
});
|
||||||
@@ -118,9 +126,12 @@ export class InvoicesService {
|
|||||||
* تفاصيل فاتورة محددة مع بنودها
|
* تفاصيل فاتورة محددة مع بنودها
|
||||||
* Get detailed information for a specific invoice including its lines
|
* Get detailed information for a specific invoice including its lines
|
||||||
*/
|
*/
|
||||||
async findOne(tenantId: string, id: string): Promise<Invoice> {
|
async findOne(user: any, id: string): Promise<Invoice> {
|
||||||
|
const isSuperAdmin = user.role === UserRole.SUPER_ADMIN;
|
||||||
|
const filter = isSuperAdmin ? { id } : { id, tenant_id: user.tenantId };
|
||||||
|
|
||||||
const invoice = await this.invoiceRepository.findOne({
|
const invoice = await this.invoiceRepository.findOne({
|
||||||
where: { id, tenant_id: tenantId },
|
where: filter,
|
||||||
relations: ['lines'],
|
relations: ['lines'],
|
||||||
});
|
});
|
||||||
if (!invoice) throw new NotFoundException('Invoice not found');
|
if (!invoice) throw new NotFoundException('Invoice not found');
|
||||||
@@ -131,8 +142,8 @@ export class InvoicesService {
|
|||||||
* تحديث بيانات الفاتورة يدوياً
|
* تحديث بيانات الفاتورة يدوياً
|
||||||
* Manually update invoice data (only if not already submitted)
|
* Manually update invoice data (only if not already submitted)
|
||||||
*/
|
*/
|
||||||
async update(tenantId: string, id: string, updateData: any): Promise<Invoice> {
|
async update(user: any, id: string, updateData: any): Promise<Invoice> {
|
||||||
const invoice = await this.findOne(tenantId, id);
|
const invoice = await this.findOne(user, id);
|
||||||
|
|
||||||
if (invoice.status === InvoiceStatus.APPROVED || invoice.status === InvoiceStatus.SUBMITTING) {
|
if (invoice.status === InvoiceStatus.APPROVED || invoice.status === InvoiceStatus.SUBMITTING) {
|
||||||
throw new ForbiddenException('Cannot edit already approved or submitted invoice');
|
throw new ForbiddenException('Cannot edit already approved or submitted invoice');
|
||||||
@@ -146,15 +157,15 @@ export class InvoicesService {
|
|||||||
* إرسال الفاتورة إلى بوابة جو فوترة الحكومية (ISTD)
|
* إرسال الفاتورة إلى بوابة جو فوترة الحكومية (ISTD)
|
||||||
* Submit the validated invoice to the official JoFotara portal
|
* Submit the validated invoice to the official JoFotara portal
|
||||||
*/
|
*/
|
||||||
async submitToJoFotara(tenantId: string, id: string): Promise<any> {
|
async submitToJoFotara(user: any, id: string): Promise<any> {
|
||||||
const invoice = await this.findOne(tenantId, id);
|
const invoice = await this.findOne(user, id);
|
||||||
|
|
||||||
if (invoice.status !== InvoiceStatus.VALIDATED && invoice.status !== InvoiceStatus.VALIDATION_FAILED && invoice.status !== InvoiceStatus.EXTRACTED) {
|
if (invoice.status !== InvoiceStatus.VALIDATED && invoice.status !== InvoiceStatus.VALIDATION_FAILED && invoice.status !== InvoiceStatus.EXTRACTED) {
|
||||||
throw new ForbiddenException('Invoice must be validated or extracted before submission');
|
throw new ForbiddenException('Invoice must be validated or extracted before submission');
|
||||||
}
|
}
|
||||||
|
|
||||||
const company = await this.companiesService.findOne(tenantId, invoice.company_id);
|
const company = await this.companiesService.findOne(user, invoice.company_id);
|
||||||
const credentials = await this.companiesService.getDecryptedCredentials(tenantId, invoice.company_id);
|
const credentials = await this.companiesService.getDecryptedCredentials(user, invoice.company_id);
|
||||||
|
|
||||||
const xml = this.ublGenerator.generateXML(invoice, company);
|
const xml = this.ublGenerator.generateXML(invoice, company);
|
||||||
|
|
||||||
@@ -182,8 +193,8 @@ export class InvoicesService {
|
|||||||
* حذف الفاتورة والملف التابع لها نهائياً
|
* حذف الفاتورة والملف التابع لها نهائياً
|
||||||
* Permanently delete an invoice and its associated file from storage
|
* Permanently delete an invoice and its associated file from storage
|
||||||
*/
|
*/
|
||||||
async remove(tenantId: string, id: string): Promise<void> {
|
async remove(user: any, id: string): Promise<void> {
|
||||||
const invoice = await this.findOne(tenantId, id);
|
const invoice = await this.findOne(user, id);
|
||||||
|
|
||||||
// 1. Delete file if exists
|
// 1. Delete file if exists
|
||||||
if (invoice.original_file_path) {
|
if (invoice.original_file_path) {
|
||||||
@@ -198,8 +209,8 @@ export class InvoicesService {
|
|||||||
* الحصول على الملف كـ Stream لتحميله أو عرضه
|
* الحصول على الملف كـ Stream لتحميله أو عرضه
|
||||||
* Get the original invoice file as a streamable file for download/view
|
* Get the original invoice file as a streamable file for download/view
|
||||||
*/
|
*/
|
||||||
async getFile(tenantId: string, id: string): Promise<StreamableFile> {
|
async getFile(user: any, id: string): Promise<StreamableFile> {
|
||||||
const invoice = await this.findOne(tenantId, id);
|
const invoice = await this.findOne(user, id);
|
||||||
if (!invoice.original_file_path) {
|
if (!invoice.original_file_path) {
|
||||||
throw new NotFoundException('Invoice file not found');
|
throw new NotFoundException('Invoice file not found');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ export enum UserRole {
|
|||||||
ACCOUNTANT = 'accountant', // محاسب (قادر على رفع الفواتير وإدارتها لكل الشركات)
|
ACCOUNTANT = 'accountant', // محاسب (قادر على رفع الفواتير وإدارتها لكل الشركات)
|
||||||
CLIENT = 'client', // عميل (قادر على رفع وإدارة فواتير شركته فقط)
|
CLIENT = 'client', // عميل (قادر على رفع وإدارة فواتير شركته فقط)
|
||||||
VIEWER = 'viewer', // مشاهد (قادر على الاطلاع فقط)
|
VIEWER = 'viewer', // مشاهد (قادر على الاطلاع فقط)
|
||||||
|
SUPER_ADMIN = 'super_admin', // مدير النظام العام (قادر على رؤية كل شيء)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,25 +26,25 @@ export class UsersController {
|
|||||||
constructor(private usersService: UsersService) {}
|
constructor(private usersService: UsersService) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@Roles(UserRole.ADMIN)
|
@Roles(UserRole.ADMIN, UserRole.SUPER_ADMIN)
|
||||||
async create(@CurrentUser() user: any, @Body() dto: any) {
|
async create(@CurrentUser() user: any, @Body() dto: any) {
|
||||||
return this.usersService.create(user.tenantId, dto);
|
return this.usersService.create(user, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
async findAll(@CurrentUser() user: any) {
|
async findAll(@CurrentUser() user: any) {
|
||||||
return this.usersService.findAll(user.tenantId);
|
return this.usersService.findAll(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
async findOne(@CurrentUser() user: any, @Param('id') id: string) {
|
async findOne(@CurrentUser() user: any, @Param('id') id: string) {
|
||||||
return this.usersService.findOne(user.tenantId, id);
|
return this.usersService.findOne(user, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@Roles(UserRole.ADMIN)
|
@Roles(UserRole.ADMIN, UserRole.SUPER_ADMIN)
|
||||||
async remove(@CurrentUser() user: any, @Param('id') id: string) {
|
async remove(@CurrentUser() user: any, @Param('id') id: string) {
|
||||||
return this.usersService.remove(user.tenantId, id, user.id);
|
return this.usersService.remove(user, id, user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('profile')
|
@Post('profile')
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import { User } from './entities/user.entity';
|
import { User } from './entities/user.entity';
|
||||||
|
import { UserRole } from './enums/role.enum';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UsersService {
|
export class UsersService {
|
||||||
@@ -20,7 +21,8 @@ export class UsersService {
|
|||||||
/**
|
/**
|
||||||
* إضافة مستخدم لمكتب محاسبة
|
* إضافة مستخدم لمكتب محاسبة
|
||||||
*/
|
*/
|
||||||
async create(tenantId: string, dto: any): Promise<User> {
|
async create(user: any, dto: any): Promise<User> {
|
||||||
|
const tenantId = user.tenantId;
|
||||||
const normalizedEmail = dto.email?.trim().toLowerCase();
|
const normalizedEmail = dto.email?.trim().toLowerCase();
|
||||||
const existing = await this.userRepository.findOne({
|
const existing = await this.userRepository.findOne({
|
||||||
where: { email: normalizedEmail, tenant_id: tenantId },
|
where: { email: normalizedEmail, tenant_id: tenantId },
|
||||||
@@ -32,22 +34,25 @@ export class UsersService {
|
|||||||
|
|
||||||
const passwordHash = await bcrypt.hash(dto.password, 12);
|
const passwordHash = await bcrypt.hash(dto.password, 12);
|
||||||
|
|
||||||
const user = this.userRepository.create({
|
const newUser = this.userRepository.create({
|
||||||
...dto,
|
...dto,
|
||||||
email: normalizedEmail,
|
email: normalizedEmail,
|
||||||
password_hash: passwordHash,
|
password_hash: passwordHash,
|
||||||
tenant_id: tenantId,
|
tenant_id: tenantId,
|
||||||
} as Partial<User>);
|
} as Partial<User>);
|
||||||
|
|
||||||
return this.userRepository.save(user);
|
return this.userRepository.save(newUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* قائمة مستخدمي المكتب
|
* قائمة مستخدمي المكتب
|
||||||
*/
|
*/
|
||||||
async findAll(tenantId: string): Promise<User[]> {
|
async findAll(user: any): Promise<User[]> {
|
||||||
|
const isSuperAdmin = user.role === UserRole.SUPER_ADMIN;
|
||||||
|
const filter = isSuperAdmin ? { is_active: true } : { tenant_id: user.tenantId, is_active: true };
|
||||||
|
|
||||||
return this.userRepository.find({
|
return this.userRepository.find({
|
||||||
where: { tenant_id: tenantId, is_active: true },
|
where: filter,
|
||||||
order: { created_at: 'ASC' },
|
order: { created_at: 'ASC' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -55,22 +60,25 @@ export class UsersService {
|
|||||||
/**
|
/**
|
||||||
* تفاصيل مستخدم
|
* تفاصيل مستخدم
|
||||||
*/
|
*/
|
||||||
async findOne(tenantId: string, id: string): Promise<User> {
|
async findOne(user: any, id: string): Promise<User> {
|
||||||
const user = await this.userRepository.findOne({
|
const isSuperAdmin = user.role === UserRole.SUPER_ADMIN;
|
||||||
where: { id, tenant_id: tenantId },
|
const filter = isSuperAdmin ? { id } : { id, tenant_id: user.tenantId };
|
||||||
|
|
||||||
|
const foundUser = await this.userRepository.findOne({
|
||||||
|
where: filter,
|
||||||
});
|
});
|
||||||
if (!user) throw new NotFoundException('User not found');
|
if (!foundUser) throw new NotFoundException('User not found');
|
||||||
return user;
|
return foundUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* تعطيل مستخدم
|
* تعطيل مستخدم
|
||||||
*/
|
*/
|
||||||
async remove(tenantId: string, id: string, currentUserId: string): Promise<void> {
|
async remove(user: any, id: string, currentUserId: string): Promise<void> {
|
||||||
if (id === currentUserId) {
|
if (id === currentUserId) {
|
||||||
throw new ConflictException('لا يمكنك تعطيل حسابك الشخصي');
|
throw new ConflictException('لا يمكنك تعطيل حسابك الشخصي');
|
||||||
}
|
}
|
||||||
const user = await this.findOne(tenantId, id);
|
await this.findOne(user, id);
|
||||||
await this.userRepository.update(id, { is_active: false });
|
await this.userRepository.update(id, { is_active: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user