feat(billing): cap each owner's stored file volume

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-02 15:44:36 +03:30
co-authored by Cursor
parent e6e90aabf3
commit 28938e8f77
20 changed files with 647 additions and 22 deletions
+22 -11
View File
@@ -15,6 +15,7 @@ import type {
} from '@mento/shared';
import { PrismaService } from '../prisma/prisma.service';
import { LocalFileStorage } from '../storage/local-file.storage';
import { StorageQuotaService } from '../billing/storage-quota.service';
import type { Actor } from '../common/record-scope';
import { TreasuryReceivablesService } from './treasury-receivables.service';
@@ -61,6 +62,7 @@ export class TreasuryService {
private readonly prisma: PrismaService,
private readonly receivables: TreasuryReceivablesService,
private readonly files: LocalFileStorage,
private readonly storage: StorageQuotaService,
) {}
async ensureCategories(organizationId: string) {
@@ -613,18 +615,27 @@ export class TreasuryService {
if (!doc) throw new NotFoundException('سند پیدا نشد.');
const safeName = file.originalname.replace(/[^\w.\-\u0600-\u06FF]+/g, '_').slice(0, 80);
const storageKey = `treasury/${actor.organizationId}/${doc.id}/${randomUUID()}-${safeName || 'file'}`;
const bytes = file.buffer.length;
await this.storage.assertCanAdd(actor.organizationId, bytes);
await this.files.save(storageKey, file.buffer);
return this.prisma.financialAttachment.create({
data: {
organizationId: actor.organizationId,
documentId: doc.id,
fileName: file.originalname,
mimeType: file.mimetype || 'application/octet-stream',
storageKey,
createdById: actor.userId,
},
select: { id: true, fileName: true, createdAt: true },
});
try {
const row = await this.prisma.financialAttachment.create({
data: {
organizationId: actor.organizationId,
documentId: doc.id,
fileName: file.originalname,
mimeType: file.mimetype || 'application/octet-stream',
storageKey,
createdById: actor.userId,
},
select: { id: true, fileName: true, createdAt: true },
});
await this.storage.addLocal(actor.organizationId, bytes);
return row;
} catch (err) {
await this.files.remove(storageKey);
throw err;
}
}
async dashboard(