From d65705732ea633e78a57c52ad72ffb398fc4de8a Mon Sep 17 00:00:00 2001 From: Michael Dausmann Date: Sun, 10 Sep 2023 22:46:32 +1000 Subject: [PATCH] useage limits on OpenAI requests. closes #12 --- CHANGELOG.md | 5 +++ README.md | 1 + lib/services/account.service.ts | 66 +++++++++++++++++++++++++++++ lib/services/notes.service.ts | 9 +++- package-lock.json | 4 +- package.json | 2 +- pages/account.vue | 5 +++ prisma/schema.prisma | 3 ++ prisma/seed.ts | 3 ++ server/trpc/routers/notes.router.ts | 2 +- 10 files changed, 95 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a3cf80..e34b6c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Version 1.3.0 +- Add an example of usage limits (Notes AI Gen). +- Includes non-destructive schema changes +```npx prisma db push``` + ## Version 1.2.0 - 'Lift' auth context into server middleware to support authenticated api (rest) endpoints for alternate clients while still supporting fully typed Trpc context. diff --git a/README.md b/README.md index d0d6ff5..1d56c64 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ Discord [here](https://discord.gg/3hWPDTA4kD) - [x] Max Notes enforced - [x] Add, Delete notes on Dashboard - [x] AI Note generation with OpenAI +- [x] Per Account, Per Month Useage Limits on AI Access ### Testing - [x] Manual test scenario for auth and sub workflows passing diff --git a/lib/services/account.service.ts b/lib/services/account.service.ts index e4f90c3..d215a6d 100644 --- a/lib/services/account.service.ts +++ b/lib/services/account.service.ts @@ -2,6 +2,10 @@ import { ACCOUNT_ACCESS } from '~~/prisma/account-access-enum'; import prisma_client from '~~/prisma/prisma.client'; import { accountWithMembers, AccountWithMembers, membershipWithAccount, MembershipWithAccount, membershipWithUser, MembershipWithUser } from './service.types'; import generator from 'generate-password-ts'; +import { UtilService } from './util.service'; +import { AccountLimitError } from './errors'; + +const config = useRuntimeConfig(); export default class AccountService { async getAccountById(account_id: number): Promise { @@ -50,6 +54,7 @@ export default class AccountService { data: { stripe_subscription_id, current_period_ends, + ai_gen_count:0, } }); } else { @@ -64,6 +69,8 @@ export default class AccountService { max_notes: paid_plan.max_notes, max_members: paid_plan.max_members, plan_name: paid_plan.name, + ai_gen_max_pm: paid_plan.ai_gen_max_pm, + ai_gen_count:0, // I did vacillate on this point ultimately easier to just reset, discussion here https://www.reddit.com/r/SaaS/comments/16e9bew/should_i_reset_usage_counts_on_plan_upgrade/ } }); } @@ -272,4 +279,63 @@ export default class AccountService { } }); } + + /* + **** Usage Limit Checking ***** + This is trickier than you might think at first. Free plan users don't get a webhook from Stripe + that we can use to tick over their period end date and associated usage counts. I also didn't + want to require an additional background thread to do the rollover processing. + + getAccountWithPeriodRollover: retrieves an account record and does the rollover checking returning up to date account info + checkAIGenCount: retrieves the account using getAccountWithPeriodRollover, checks the count and returns the account + incrementAIGenCount: increments the counter using the account. Note that passing in the account avoids another db fetch for the account. + + Note.. for each usage limit, you will need another pair of check/increment methods and of course the count and max limit in the account schema + + How to use in a service method.... + async someServiceMethod(account_id: number, .....etc) { + const accountService = new AccountService(); + const account = await accountService.checkAIGenCount(account_id); + ... User is under the limit so do work + await accountService.incrementAIGenCount(account); + } + */ + + async getAccountWithPeriodRollover (account_id: number){ + const account = await prisma_client.account.findFirstOrThrow({ + where: { id: account_id } + }); + + if(account.plan_name === config.initialPlanName && account.current_period_ends < new Date()){ + return await prisma_client.account.update({ + where: { id: account.id }, + data: { + current_period_ends: UtilService.addMonths(account.current_period_ends,1), + // reset anything that is affected by the rollover + ai_gen_count: 0, + }, + }); + }; + + return account; + } + + async checkAIGenCount(account_id: number){ + const account = await this.getAccountWithPeriodRollover(account_id); + + if(account.ai_gen_count >= account.ai_gen_max_pm){ + throw new AccountLimitError('Monthly AI gen limit reached, no new AI Generations can be made'); + } + + return account; + } + + async incrementAIGenCount (account: any){ + return await prisma_client.account.update({ + where: { id: account.id }, + data: { + ai_gen_count: account.ai_gen_count + 1, + } + }); + } } diff --git a/lib/services/notes.service.ts b/lib/services/notes.service.ts index 5865aa7..b54d3df 100644 --- a/lib/services/notes.service.ts +++ b/lib/services/notes.service.ts @@ -1,6 +1,7 @@ import prisma_client from '~~/prisma/prisma.client'; import { openai } from './openai.client'; import { AccountLimitError } from './errors'; +import AccountService from './account.service'; export default class NotesService { async getAllNotes() { @@ -36,7 +37,10 @@ export default class NotesService { return prisma_client.note.delete({ where: { id } }); } - async generateAINoteFromPrompt(userPrompt: string) { + async generateAINoteFromPrompt(userPrompt: string, account_id: number) { + const accountService = new AccountService(); + const account = await accountService.checkAIGenCount(account_id); + const prompt = ` Write an interesting short note about ${userPrompt}. Restrict the note to a single paragraph. @@ -49,6 +53,9 @@ export default class NotesService { max_tokens: 1000, n: 1, }); + + await accountService.incrementAIGenCount(account); + return completion.data.choices[0].text; } } diff --git a/package-lock.json b/package-lock.json index 725c363..bf325b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "supanuxt-saas", - "version": "1.2.0", + "version": "1.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "supanuxt-saas", - "version": "1.2.0", + "version": "1.3.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index d8baac5..6dff6d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "supanuxt-saas", - "version": "1.2.0", + "version": "1.3.0", "author": { "name": "Michael Dausmann", "email": "mdausmann@gmail.com", diff --git a/pages/account.vue b/pages/account.vue index e915299..fcbbb77 100644 --- a/pages/account.vue +++ b/pages/account.vue @@ -60,6 +60,11 @@ {{ activeMembership?.account.max_notes }} +
+ AI Gens for this Month/Max: + {{ activeMembership?.account.ai_gen_count }} / {{ activeMembership?.account.ai_gen_max_pm }} +
+
Maximum Members: {{ activeMembership?.account.max_members }} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 49b4cb6..5d94068 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -56,6 +56,8 @@ model Account { stripe_customer_id String? max_members Int @default(1) join_password String @unique + ai_gen_max_pm Int @default(7) + ai_gen_count Int @default(0) @@map("account") } @@ -68,6 +70,7 @@ model Plan { max_notes Int @default(100) stripe_product_id String? max_members Int @default(1) + ai_gen_max_pm Int @default(7) @@map("plan") } diff --git a/prisma/seed.ts b/prisma/seed.ts index 565d811..570a4b3 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -9,6 +9,7 @@ async function main() { features: ['ADD_NOTES', 'EDIT_NOTES', 'VIEW_NOTES'], max_notes: 10, max_members: 1, + ai_gen_max_pm: 7, }, }); const individualPlan = await prisma.plan.upsert({ @@ -19,6 +20,7 @@ async function main() { features: ['ADD_NOTES', 'EDIT_NOTES', 'VIEW_NOTES', 'SPECIAL_FEATURE'], max_notes: 100, max_members: 1, + ai_gen_max_pm: 50, stripe_product_id: 'prod_NQR7vwUulvIeqW' }, }); @@ -30,6 +32,7 @@ async function main() { features: ['ADD_NOTES', 'EDIT_NOTES', 'VIEW_NOTES', 'SPECIAL_FEATURE', 'SPECIAL_TEAM_FEATURE'], max_notes: 200, max_members: 10, + ai_gen_max_pm: 500, stripe_product_id: 'prod_NQR8IkkdhqBwu2' }, }); diff --git a/server/trpc/routers/notes.router.ts b/server/trpc/routers/notes.router.ts index 5f1812b..484d4f7 100644 --- a/server/trpc/routers/notes.router.ts +++ b/server/trpc/routers/notes.router.ts @@ -42,7 +42,7 @@ export const notesRouter = router({ .input(z.object({ user_prompt: z.string() })) .query(async ({ ctx, input }) => { const notesService = new NotesService(); - const noteText = (ctx.activeAccountId)?await notesService.generateAINoteFromPrompt(input.user_prompt):null; + const noteText = (ctx.activeAccountId)?await notesService.generateAINoteFromPrompt(input.user_prompt, ctx.activeAccountId):null; return { noteText }