diff --git a/packages/backend/src/controllers/api/v1/users/get-plan-and-usage.ee.js b/packages/backend/src/controllers/api/v1/users/get-plan-and-usage.ee.js new file mode 100644 index 00000000..bda4c4f1 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/users/get-plan-and-usage.ee.js @@ -0,0 +1,7 @@ +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const planAndUsage = await request.currentUser.getPlanAndUsage(); + + renderObject(response, planAndUsage); +}; diff --git a/packages/backend/src/controllers/api/v1/users/get-plan-and-usage.ee.test.js b/packages/backend/src/controllers/api/v1/users/get-plan-and-usage.ee.test.js new file mode 100644 index 00000000..239a6e36 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/users/get-plan-and-usage.ee.test.js @@ -0,0 +1,68 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js'; +import { createUser } from '../../../../../test/factories/user.js'; +import { createSubscription } from '../../../../../test/factories/subscription.js'; +import { createUsageData } from '../../../../../test/factories/usage-data.js'; +import appConfig from '../../../../config/app.js'; +import { DateTime } from 'luxon'; + +describe('GET /api/v1/users/:userId/plan-and-usage', () => { + let user, token; + + beforeEach(async () => { + const trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate(); + user = await createUser({ trialExpiryDate }); + token = createAuthTokenByUserId(user.id); + + vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true); + }); + + it('should return free trial plan and usage data', async () => { + const response = await request(app) + .get(`/api/v1/users/${user.id}/plan-and-usage`) + .set('Authorization', token) + .expect(200); + + const expectedResponseData = { + plan: { + id: null, + limit: null, + name: 'Free Trial', + }, + usage: { + task: 0, + }, + }; + + expect(response.body.data).toEqual(expectedResponseData); + }); + + it('should return current plan and usage data', async () => { + await createSubscription({ userId: user.id }); + + await createUsageData({ + userId: user.id, + consumedTaskCount: 1234, + }); + + const response = await request(app) + .get(`/api/v1/users/${user.id}/plan-and-usage`) + .set('Authorization', token) + .expect(200); + + const expectedResponseData = { + plan: { + id: '47384', + limit: '10,000', + name: '10k - monthly', + }, + usage: { + task: 1234, + }, + }; + + expect(response.body.data).toEqual(expectedResponseData); + }); +}); diff --git a/packages/backend/src/models/user.js b/packages/backend/src/models/user.js index d3dcaffe..dfc68d1c 100644 --- a/packages/backend/src/models/user.js +++ b/packages/backend/src/models/user.js @@ -255,6 +255,31 @@ class User extends Base { return currentUsageData.consumedTaskCount < plan.quota; } + async getPlanAndUsage() { + const usageData = await this.$relatedQuery( + 'currentUsageData' + ).throwIfNotFound(); + + const subscription = await this.$relatedQuery('currentSubscription'); + + const currentPlan = Billing.paddlePlans.find( + (plan) => plan.productId === subscription?.paddlePlanId + ); + + const planAndUsage = { + usage: { + task: usageData.consumedTaskCount, + }, + plan: { + id: subscription?.paddlePlanId || null, + name: subscription ? currentPlan.name : 'Free Trial', + limit: currentPlan?.limit || null, + }, + }; + + return planAndUsage; + } + async getInvoices() { const subscription = await this.$relatedQuery('currentSubscription'); diff --git a/packages/backend/src/routes/api/v1/users.js b/packages/backend/src/routes/api/v1/users.js index 51d5cca1..ab1d1d7f 100644 --- a/packages/backend/src/routes/api/v1/users.js +++ b/packages/backend/src/routes/api/v1/users.js @@ -6,6 +6,7 @@ import getCurrentUserAction from '../../../controllers/api/v1/users/get-current- import getUserTrialAction from '../../../controllers/api/v1/users/get-user-trial.ee.js'; import getInvoicesAction from '../../../controllers/api/v1/users/get-invoices.ee.js'; import getSubscriptionAction from '../../../controllers/api/v1/users/get-subscription.ee.js'; +import getPlanAndUsageAction from '../../../controllers/api/v1/users/get-plan-and-usage.ee.js'; const router = Router(); @@ -31,4 +32,11 @@ router.get( asyncHandler(getSubscriptionAction) ); +router.get( + '/:userId/plan-and-usage', + authenticateUser, + checkIsCloud, + asyncHandler(getPlanAndUsageAction) +); + export default router; diff --git a/packages/backend/test/factories/usage-data.js b/packages/backend/test/factories/usage-data.js new file mode 100644 index 00000000..c6d07614 --- /dev/null +++ b/packages/backend/test/factories/usage-data.js @@ -0,0 +1,15 @@ +import { DateTime } from 'luxon'; +import { createUser } from './user'; +import UsageData from '../../src/models/usage-data.ee.js'; + +export const createUsageData = async (params = {}) => { + params.userId = params?.userId || (await createUser()).id; + params.nextResetAt = + params?.nextResetAt || DateTime.now().plus({ days: 30 }).toISODate(); + + params.consumedTaskCount = params?.consumedTaskCount || 0; + + const usageData = await UsageData.query().insertAndFetch(params); + + return usageData; +};