feat: better credit system to prevent spam

This commit is contained in:
Nevo David 2025-07-14 15:51:39 +07:00
parent 8285bb0ddf
commit a214815f81
6 changed files with 65 additions and 31 deletions

View File

@ -1,5 +1,5 @@
import { Global, Module } from '@nestjs/common';
import { PrismaRepository, PrismaService } from './prisma.service';
import { PrismaRepository, PrismaService, PrismaTransaction } from './prisma.service';
import { OrganizationRepository } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.repository';
import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service';
import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service';
@ -49,6 +49,7 @@ import { FalService } from '@gitroom/nestjs-libraries/openai/fal.service';
providers: [
PrismaService,
PrismaRepository,
PrismaTransaction,
UsersService,
UsersRepository,
OrganizationService,

View File

@ -37,16 +37,17 @@ export class MediaService {
org: Organization,
generatePromptFirst?: boolean
) {
if (generatePromptFirst) {
prompt = await this._openAi.generatePromptForPicture(prompt);
console.log('Prompt:', prompt);
}
const image = await this._openAi.generateImage(
prompt,
!!generatePromptFirst
return await this._subscriptionService.useCredit(
org,
'ai_images',
async () => {
if (generatePromptFirst) {
prompt = await this._openAi.generatePromptForPicture(prompt);
console.log('Prompt:', prompt);
}
return this._openAi.generateImage(prompt, !!generatePromptFirst);
}
);
await this._subscriptionService.useCredit(org);
return image;
}
saveFile(org: string, fileName: string, filePath: string) {
@ -99,17 +100,21 @@ export class MediaService {
throw new HttpException('This video is not available in trial mode', 406);
}
const loadedData = await video.instance.processAndValidate(
body.output,
body.customParams
await video.instance.processAndValidate(body.customParams);
return await this._subscriptionService.useCredit(
org,
'ai_videos',
async () => {
const loadedData = await video.instance.process(
body.output,
body.customParams
);
const file = await this.storage.uploadSimple(loadedData);
return this.saveFile(org.id, file.split('/').pop(), file);
}
);
const file = await this.storage.uploadSimple(loadedData);
const save = await this.saveFile(org.id, file.split('/').pop(), file);
await this._subscriptionService.useCredit(org, 'ai_videos');
return save;
}
async videoFunction(identifier: string, functionName: string, body: any) {

View File

@ -25,3 +25,12 @@ export class PrismaRepository<T extends keyof PrismaService> {
this.model = this._prismaService;
}
}
@Injectable()
export class PrismaTransaction {
public model: Pick<PrismaService, '$transaction'>;
constructor(private _prismaService: PrismaService) {
this.model = this._prismaService;
}
}

View File

@ -1,5 +1,8 @@
import { Injectable } from '@nestjs/common';
import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
import {
PrismaRepository,
PrismaTransaction,
} from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
import dayjs from 'dayjs';
import { Organization } from '@prisma/client';
@ -203,7 +206,11 @@ export class SubscriptionRepository {
});
}
async getCreditsFrom(organizationId: string, from: dayjs.Dayjs, type = 'ai_images') {
async getCreditsFrom(
organizationId: string,
from: dayjs.Dayjs,
type = 'ai_images'
) {
const load = await this._credits.model.credits.groupBy({
by: ['organizationId'],
where: {
@ -221,14 +228,29 @@ export class SubscriptionRepository {
return load?.[0]?._sum?.credits || 0;
}
useCredit(org: Organization, type = 'ai_images') {
return this._credits.model.credits.create({
async useCredit<T>(
org: Organization,
type = 'ai_images',
func: () => Promise<T>
) {
const data = await this._credits.model.credits.create({
data: {
organizationId: org.id,
credits: 1,
type,
},
});
try {
return await func();
} catch (err) {
await this._credits.model.credits.delete({
where: {
id: data.id,
},
});
throw err;
}
}
setCustomerId(orgId: string, customerId: string) {

View File

@ -21,8 +21,8 @@ export class SubscriptionService {
);
}
useCredit(organization: Organization, type = 'ai_images') {
return this._subscriptionRepository.useCredit(organization, type);
useCredit<T>(organization: Organization, type = 'ai_images', func: () => Promise<T>) : Promise<T> {
return this._subscriptionRepository.useCredit(organization, type, func);
}
getCode(code: string) {

View File

@ -6,7 +6,6 @@ export abstract class VideoAbstract<T> {
dto: Type<T>;
async processAndValidate(
output: 'vertical' | 'horizontal',
customParams?: T
) {
const validationPipe = new ValidationPipe({
@ -17,15 +16,13 @@ export abstract class VideoAbstract<T> {
},
});
const transformed = await validationPipe.transform(customParams, {
await validationPipe.transform(customParams, {
type: 'body',
metatype: this.dto,
});
return this.process(output, transformed);
}
protected abstract process(
abstract process(
output: 'vertical' | 'horizontal',
customParams?: T
): Promise<URL>;