diff --git a/packages/backend/src/controllers/api/v1/users/get-subscription.ee.js b/packages/backend/src/controllers/api/v1/users/get-subscription.ee.js new file mode 100644 index 00000000..afecacc7 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/users/get-subscription.ee.js @@ -0,0 +1,9 @@ +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const subscription = await request.currentUser + .$relatedQuery('currentSubscription') + .throwIfNotFound(); + + renderObject(response, subscription); +}; diff --git a/packages/backend/src/controllers/api/v1/users/get-subscription.ee.test.js b/packages/backend/src/controllers/api/v1/users/get-subscription.ee.test.js new file mode 100644 index 00000000..20c918b9 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/users/get-subscription.ee.test.js @@ -0,0 +1,51 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import appConfig from '../../../../config/app.js'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id'; +import { createRole } from '../../../../../test/factories/role'; +import { createUser } from '../../../../../test/factories/user'; +import { createSubscription } from '../../../../../test/factories/subscription.js'; +import getSubscriptionMock from '../../../../../test/mocks/rest/api/v1/users/get-subscription.js'; + +describe('GET /api/v1/users/:userId/subscription', () => { + let currentUser, role, subscription, token; + + beforeEach(async () => { + vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true); + + role = await createRole(); + + currentUser = await createUser({ + roleId: role.id, + }); + + subscription = await createSubscription({ userId: currentUser.id }); + + token = createAuthTokenByUserId(currentUser.id); + }); + + it('should return subscription info of the current user', async () => { + const response = await request(app) + .get(`/api/v1/users/${currentUser.id}/subscription`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = getSubscriptionMock(subscription); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response if there is no current subscription', async () => { + const userWithoutSubscription = await createUser({ + roleId: role.id, + }); + + const token = createAuthTokenByUserId(userWithoutSubscription.id); + + await request(app) + .get(`/api/v1/users/${userWithoutSubscription.id}/subscription`) + .set('Authorization', token) + .expect(404); + }); +}); diff --git a/packages/backend/src/routes/api/v1/users.js b/packages/backend/src/routes/api/v1/users.js index 3768af3f..51d5cca1 100644 --- a/packages/backend/src/routes/api/v1/users.js +++ b/packages/backend/src/routes/api/v1/users.js @@ -5,6 +5,7 @@ import checkIsCloud from '../../../helpers/check-is-cloud.js'; import getCurrentUserAction from '../../../controllers/api/v1/users/get-current-user.js'; 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'; const router = Router(); @@ -23,4 +24,11 @@ router.get( asyncHandler(getUserTrialAction) ); +router.get( + '/:userId/subscription', + authenticateUser, + checkIsCloud, + asyncHandler(getSubscriptionAction) +); + export default router; diff --git a/packages/backend/src/serializers/index.js b/packages/backend/src/serializers/index.js index 9e0c3e82..795dd53b 100644 --- a/packages/backend/src/serializers/index.js +++ b/packages/backend/src/serializers/index.js @@ -12,6 +12,7 @@ import triggerSerializer from './trigger.js'; import actionSerializer from './action.js'; import executionSerializer from './execution.js'; import executionStepSerializer from './execution-step.js'; +import subscriptionSerializer from './subscription.ee.js'; const serializers = { User: userSerializer, @@ -28,6 +29,7 @@ const serializers = { Action: actionSerializer, Execution: executionSerializer, ExecutionStep: executionStepSerializer, + Subscription: subscriptionSerializer, }; export default serializers; diff --git a/packages/backend/src/serializers/subscription.ee.js b/packages/backend/src/serializers/subscription.ee.js new file mode 100644 index 00000000..0e2e5239 --- /dev/null +++ b/packages/backend/src/serializers/subscription.ee.js @@ -0,0 +1,20 @@ +const subscriptinSerializer = (subscription) => { + let userData = { + id: subscription.id, + paddleSubscriptionId: subscription.paddleSubscriptionId, + paddlePlanId: subscription.paddlePlanId, + updateUrl: subscription.updateUrl, + cancelUrl: subscription.cancelUrl, + status: subscription.status, + nextBillAmount: subscription.nextBillAmount, + nextBillDate: subscription.nextBillDate, + lastBillDate: subscription.lastBillDate, + createdAt: subscription.createdAt.getTime(), + updatedAt: subscription.updatedAt.getTime(), + cancellationEffectiveDate: subscription.cancellationEffectiveDate, + }; + + return userData; +}; + +export default subscriptinSerializer; diff --git a/packages/backend/src/serializers/subscription.ee.test.js b/packages/backend/src/serializers/subscription.ee.test.js new file mode 100644 index 00000000..d8195932 --- /dev/null +++ b/packages/backend/src/serializers/subscription.ee.test.js @@ -0,0 +1,35 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import appConfig from '../config/app'; +import { createUser } from '../../test/factories/user'; +import { createSubscription } from '../../test/factories/subscription'; +import subscriptionSerializer from './subscription.ee.js'; + +describe('subscriptionSerializer', () => { + let user, subscription; + + beforeEach(async () => { + user = await createUser(); + subscription = await createSubscription({ userId: user.id }); + }); + + it('should return subscription data', async () => { + vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true); + + const expectedPayload = { + id: subscription.id, + paddleSubscriptionId: subscription.paddleSubscriptionId, + paddlePlanId: subscription.paddlePlanId, + updateUrl: subscription.updateUrl, + cancelUrl: subscription.cancelUrl, + status: subscription.status, + nextBillAmount: subscription.nextBillAmount, + nextBillDate: subscription.nextBillDate, + lastBillDate: subscription.lastBillDate, + createdAt: subscription.createdAt.getTime(), + updatedAt: subscription.updatedAt.getTime(), + cancellationEffectiveDate: subscription.cancellationEffectiveDate, + }; + + expect(subscriptionSerializer(subscription)).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/test/factories/subscription.js b/packages/backend/test/factories/subscription.js new file mode 100644 index 00000000..95b158e7 --- /dev/null +++ b/packages/backend/test/factories/subscription.js @@ -0,0 +1,21 @@ +import { DateTime } from 'luxon'; +import { createUser } from './user'; +import Subscription from '../../src/models/subscription.ee.js'; + +export const createSubscription = async (params = {}) => { + params.userId = params?.userId || (await createUser()).id; + params.paddleSubscriptionId = + params?.paddleSubscriptionId || 'paddleSubscriptionId'; + + params.paddlePlanId = params?.paddlePlanId || '47384'; + params.updateUrl = params?.updateUrl || 'https://example.com/update-url'; + params.cancelUrl = params?.cancelUrl || 'https://example.com/cancel-url'; + params.status = params?.status || 'active'; + params.nextBillAmount = params?.nextBillAmount || '20'; + params.nextBillDate = + params?.nextBillDate || DateTime.now().plus({ days: 30 }).toISODate(); + + const subscription = await Subscription.query().insert(params).returning('*'); + + return subscription; +}; diff --git a/packages/backend/test/mocks/rest/api/v1/users/get-subscription.js b/packages/backend/test/mocks/rest/api/v1/users/get-subscription.js new file mode 100644 index 00000000..7c0d3bc5 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/users/get-subscription.js @@ -0,0 +1,27 @@ +const getSubscriptionMock = (subscription) => { + return { + data: { + id: subscription.id, + paddlePlanId: subscription.paddlePlanId, + paddleSubscriptionId: subscription.paddleSubscriptionId, + cancelUrl: subscription.cancelUrl, + updateUrl: subscription.updateUrl, + status: subscription.status, + nextBillAmount: subscription.nextBillAmount, + nextBillDate: subscription.nextBillDate.toISOString(), + lastBillDate: subscription.lastBillDate, + cancellationEffectiveDate: subscription.cancellationEffectiveDate, + createdAt: subscription.createdAt.getTime(), + updatedAt: subscription.updatedAt.getTime(), + }, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Subscription', + }, + }; +}; + +export default getSubscriptionMock;