Skip to content

Commit

Permalink
Feat/conditional feature access based on user plan and payment (#728)
Browse files Browse the repository at this point in the history
  • Loading branch information
chavda-bhavik authored Aug 7, 2024
2 parents 8dd1347 + 7bb2b86 commit 37c5df0
Show file tree
Hide file tree
Showing 28 changed files with 280 additions and 78 deletions.
2 changes: 1 addition & 1 deletion apps/api/src/app/common/common.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class CommonController {
summary: 'Check if request is valid (Checks Auth)',
})
async isRequestValid(@Body() body: ValidRequestDto): Promise<{ success: boolean }> {
return this.validRequest.execute(
return await this.validRequest.execute(
ValidRequestCommand.create({
projectId: body.projectId,
templateId: body.templateId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { UserRepository, TemplateRepository, TemplateEntity } from '@impler/dal';
import { IImportConfig } from '@impler/shared';
import { AVAILABLE_BILLABLEMETRIC_CODE_ENUM, IImportConfig } from '@impler/shared';
import { PaymentAPIService } from '@impler/services';
import { APIMessages } from '@shared/constants';

Expand All @@ -15,7 +15,10 @@ export class GetImportConfig {
async execute(projectId: string, templateId?: string): Promise<IImportConfig> {
const userEmail = await this.userRepository.findUserEmailFromProjectId(projectId);

const removeBrandingAvailable = await this.paymentAPIService.checkEvent(userEmail, 'REMOVE_BRANDING');
const removeBrandingAvailable = await this.paymentAPIService.checkEvent({
email: userEmail,
billableMetricCode: AVAILABLE_BILLABLEMETRIC_CODE_ENUM.REMOVE_BRANDING,
});

let template: TemplateEntity;
if (templateId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@ import { Injectable, HttpStatus, HttpException, UnauthorizedException } from '@n
import { APIMessages } from '@shared/constants';
import { SchemaDto } from 'app/common/dtos/Schema.dto';
import { ValidRequestCommand } from './valid-request.command';
import { ProjectRepository, TemplateRepository } from '@impler/dal';
import { ProjectRepository, TemplateRepository, UserEntity } from '@impler/dal';
import { UniqueColumnException } from '@shared/exceptions/unique-column.exception';
import { DocumentNotFoundException } from '@shared/exceptions/document-not-found.exception';
import { PaymentAPIService } from '@impler/services';
import { AVAILABLE_BILLABLEMETRIC_CODE_ENUM } from '@impler/shared';

@Injectable()
export class ValidRequest {
constructor(
private projectRepository: ProjectRepository,
private templateRepository: TemplateRepository
private templateRepository: TemplateRepository,
private paymentAPIService: PaymentAPIService
) {}

async execute(command: ValidRequestCommand): Promise<{ success: boolean }> {
Expand All @@ -38,6 +41,22 @@ export class ValidRequest {
throw new DocumentNotFoundException('Template', command.templateId, APIMessages.INCORRECT_KEYS_FOUND);
}
}
if (command.schema) {
const project = await this.projectRepository.getUserOfProject(command.projectId);

const isBillableMetricAvailable = await this.paymentAPIService.checkEvent({
email: (project._userId as unknown as UserEntity).email,
billableMetricCode: AVAILABLE_BILLABLEMETRIC_CODE_ENUM.IMAGE_UPLOAD,
});

if (!isBillableMetricAvailable) {
throw new DocumentNotFoundException(
'Schema',
command.schema,
APIMessages.ERROR_ACCESSING_FEATURE.IMAGE_UPLOAD
);
}
}

if (command.schema) {
const parsedSchema: SchemaDto[] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ export class StartProcess {
let importedData;
const destination = (uploadInfo._templateId as unknown as TemplateEntity)?.destination;
const userEmail = await this.uploadRepository.getUserEmailFromUploadId(_uploadId);
const dataProcessingAllowed = await this.paymentAPIService.checkEvent(userEmail);
const dataProcessingAllowed = await this.paymentAPIService.checkEvent({
email: userEmail,
});

if (
dataProcessingAllowed &&
Expand Down
3 changes: 3 additions & 0 deletions apps/api/src/app/shared/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export const APIMessages = {
COLUMN_KEY_DUPLICATED: 'Column with the same key already exists. Please provide a unique key.',
ERROR_DURING_VALIDATION:
'Something went wrong while validating data. Data is not imported yet, but team is informed about issue. Please try again after sometime.',
ERROR_ACCESSING_FEATURE: {
IMAGE_UPLOAD: 'You do not have access to Image Upload Functionality.',
},
};

export const CONSTANTS = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export class GetTemplateDetails {
async execute(_id: string): Promise<TemplateResponseDto> {
const template = await this.templateRepository.findOne(
{ _id },
'_projectId name sampleFileUrl _id totalUploads totalInvalidRecords totalRecords'
'_projectId name sampleFileUrl _id totalUploads totalInvalidRecords totalRecords mode'
);
if (!template) {
throw new DocumentNotFoundException('Template', _id);
Expand Down
38 changes: 38 additions & 0 deletions apps/api/src/migrations/update-mode/update-mode.migrations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import '../../config';
import { AppModule } from '../../app.module';

import { NestFactory } from '@nestjs/core';
import { TemplateRepository } from '@impler/dal';

export async function run() {
console.log('start migration - registering payment users');

let app;
try {
// Initialize the MongoDB connection
app = await NestFactory.create(AppModule, {
logger: false,
});

const templateRepository = new TemplateRepository();

// Fetch all templates without the 'mode' field
const templatesWithoutMode = await templateRepository.find({ mode: { $exists: false } });
console.log('Templates without mode:', templatesWithoutMode);

const updateResult = await templateRepository.update({ mode: { $exists: false } }, { mode: 'manual', multi: true });

console.log('Updated templates:', updateResult);

console.log('end migration - Adding manual mode to all templates users');
} catch (error) {
console.error('An error occurred during the migration:', error);
} finally {
if (app) {
await app.close();
}
process.exit(0);
}
}

run();
4 changes: 3 additions & 1 deletion apps/queue-manager/src/consumers/end-import.consumer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ export class EndImportConsumer extends BaseConsumer {
await this.convertRecordsToJsonFile(data.uploadId);
const userEmail = await this.uploadRepository.getUserEmailFromUploadId(data.uploadId);

const dataProcessingAllowed = await this.paymentAPIService.checkEvent(userEmail);
const dataProcessingAllowed = await this.paymentAPIService.checkEvent({
email: userEmail,
});

if (dataProcessingAllowed) {
if (data.destination === DestinationsEnum.WEBHOOK) {
Expand Down
22 changes: 13 additions & 9 deletions apps/web/components/imports/forms/ColumnForm.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useEffect, useState } from 'react';
import { modals } from '@mantine/modals';
import { Controller, useForm } from 'react-hook-form';
import {
Expand All @@ -8,20 +9,22 @@ import {
SimpleGrid,
Title,
Group,
Flex,
CloseButton,
Select,
useMantineColorScheme,
Flex,
SelectItem,
} from '@mantine/core';

import { ColumnTypesEnum, DEFAULT_VALUES, IColumn } from '@impler/shared';
import { colors, COLUMN_TYPES, DELIMITERS, MODAL_KEYS, MODAL_TITLES, DOCUMENTATION_REFERENCE_LINKS } from '@config';
import { colors, DELIMITERS, MODAL_KEYS, MODAL_TITLES, DOCUMENTATION_REFERENCE_LINKS } from '@config';

import { Button } from '@ui/button';
import { Textarea } from '@ui/textarea';
import { Checkbox } from '@ui/checkbox';
import { MultiSelect } from '@ui/multi-select';
import { CustomSelect } from '@ui/custom-select';
import { useSchema } from '@hooks/useSchema';
import TooltipLink from '@components/TooltipLink/TooltipLink';

interface ColumnFormProps {
Expand All @@ -31,6 +34,8 @@ interface ColumnFormProps {
}

export function ColumnForm({ onSubmit, data, isLoading }: ColumnFormProps) {
const { getColumnTypes } = useSchema({ templateId: data?._templateId as string });
const [columnTypes, setColumnType] = useState<SelectItem[]>(getColumnTypes());
const { colorScheme } = useMantineColorScheme();
const {
watch,
Expand All @@ -48,6 +53,10 @@ export function ColumnForm({ onSubmit, data, isLoading }: ColumnFormProps) {
modals.close(MODAL_KEYS.COLUMN_UPDATE);
};

useEffect(() => {
setColumnType(getColumnTypes());
}, []);

return (
<form onSubmit={handleSubmit(onSubmit)}>
<Stack spacing="xs">
Expand Down Expand Up @@ -113,13 +122,8 @@ export function ColumnForm({ onSubmit, data, isLoading }: ColumnFormProps) {
control={control}
render={({ field: { value, onChange, onBlur } }) => (
<Select
label={
<Group spacing="xs">
<Text>Column Type</Text>
<TooltipLink link={DOCUMENTATION_REFERENCE_LINKS.primaryValidation} />
</Group>
}
data={COLUMN_TYPES}
label="Column Type"
data={columnTypes}
placeholder="Type"
value={value}
data-autofocus
Expand Down
19 changes: 10 additions & 9 deletions apps/web/components/imports/schema/ColumnsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useRef } from 'react';
import { useEffect, useRef, useState } from 'react';
import { Controller } from 'react-hook-form';
import { ActionIcon, Flex, Tooltip, TextInput as Input, Group, Badge } from '@mantine/core';
import { ActionIcon, Flex, Tooltip, TextInput as Input, Group, Badge, SelectItem } from '@mantine/core';

import { useSchema } from '@hooks/useSchema';
import { colors, COLUMN_TYPES } from '@config';
import { colors } from '@config';
import { ColumnTypesEnum, IColumn } from '@impler/shared';

import { Button } from '@ui/button';
Expand All @@ -24,6 +24,8 @@ interface ColumnsTableProps {

export function ColumnsTable({ templateId }: ColumnsTableProps) {
const ValidationRef = useRef(false);
const { getColumnTypes } = useSchema({ templateId });
const [columnTypes, setColumnType] = useState<SelectItem[]>(getColumnTypes());

const {
columns,
Expand All @@ -47,6 +49,10 @@ export function ColumnsTable({ templateId }: ColumnsTableProps) {
onValidationsClick({ ...values, key: values.key || values.name });
};

useEffect(() => {
setColumnType(getColumnTypes());
}, []);

return (
<form
onSubmit={(e) => {
Expand Down Expand Up @@ -123,12 +129,7 @@ export function ColumnsTable({ templateId }: ColumnsTableProps) {
control={control}
name="type"
render={({ field }) => (
<NativeSelect
data={COLUMN_TYPES}
placeholder="Select Type"
variant="default"
register={field}
/>
<NativeSelect data={columnTypes} placeholder="Select Type" variant="default" register={field} />
)}
/>
<Button
Expand Down
2 changes: 1 addition & 1 deletion apps/web/config/constants.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ export const DOCUMENTATION_REFERENCE_LINKS = {
bubbleIo: 'https://docs.impler.io/widget/bubble.io-embed',
subscriptionInformation: 'https://docs.impler.io/platform/how-subscription-works',
customValidation: 'https://docs.impler.io/features/custom-validation',
}
};

export const IMPORT_MODES = [
{ label: 'Manual', value: TemplateModeEnum.MANUAL },
Expand Down
19 changes: 17 additions & 2 deletions apps/web/hooks/useColumnsEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { API_KEYS } from '@config';
import { commonApi } from '@libs/api';
import { IColumn, IErrorObject } from '@impler/shared';
import { useUpdateBulkColumns } from './useUpdateBulkColumns';
import { usePlanMetaData } from 'store/planmeta.store.context';

interface UseSchemaProps {
templateId: string;
Expand All @@ -16,6 +17,7 @@ interface ColumnsData {
}

export function useColumnsEditor({ templateId }: UseSchemaProps) {
const { meta } = usePlanMetaData();
const [columnErrors, setColumnErrors] = useState<Record<number, string[]>>();
const {
control,
Expand Down Expand Up @@ -70,13 +72,26 @@ export function useColumnsEditor({ templateId }: UseSchemaProps) {
);
const onSaveColumnsClick = (data: ColumnsData) => {
setColumnErrors(undefined);
let parsedColumns;

try {
JSON.parse(data.columns);
parsedColumns = JSON.parse(data.columns);
} catch (error) {
return setError('columns', { type: 'JSON', message: 'Provided JSON is invalid!' });
}

updateColumns(JSON.parse(data.columns));
if (!meta?.IMAGE_UPLOAD) {
const imageColumnExists = parsedColumns.some((column: IColumn) => column.type === 'Image');

if (imageColumnExists) {
return setError('columns', {
type: 'Object',
message: 'Image column type is not allowed in your current plan.',
});
}
}

updateColumns(parsedColumns);
};
const onColumnUpdateError = (error: IErrorObject) => {
if (error.error && Array.isArray(error.message)) {
Expand Down
3 changes: 3 additions & 0 deletions apps/web/hooks/useImportDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import { useAppState } from 'store/app.context';
import { ITemplate, IErrorObject, IColumn } from '@impler/shared';
import { UpdateImportForm } from '@components/imports/forms/UpdateImportForm';
import { API_KEYS, MODAL_KEYS, MODAL_TITLES, NOTIFICATION_KEYS, ROUTES } from '@config';
import { usePlanMetaData } from 'store/planmeta.store.context';

interface useImportDetailProps {
templateId: string;
}

export function useImportDetails({ templateId }: useImportDetailProps) {
const { meta } = usePlanMetaData();
const router = useRouter();
const queryClient = useQueryClient();
const { profileInfo } = useAppState();
Expand Down Expand Up @@ -107,5 +109,6 @@ export function useImportDetails({ templateId }: useImportDetailProps) {
isTemplateDataLoading,
onSpreadsheetImported,
updateImport,
meta,
};
}
13 changes: 13 additions & 0 deletions apps/web/hooks/usePlanDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { useQuery } from '@tanstack/react-query';
import { commonApi } from '@libs/api';
import { API_KEYS } from '@config';
import { IErrorObject } from '@impler/shared';
import { usePlanMetaData } from 'store/planmeta.store.context';

interface UsePlanDetailProps {
email: string;
}

export function usePlanDetails({ email }: UsePlanDetailProps) {
const { meta, setPlanMeta } = usePlanMetaData();
const { data: activePlanDetails, isLoading: isActivePlanLoading } = useQuery<
unknown,
IErrorObject,
Expand All @@ -17,12 +19,23 @@ export function usePlanDetails({ email }: UsePlanDetailProps) {
[API_KEYS.FETCH_ACTIVE_SUBSCRIPTION, email],
() => commonApi<ISubscriptionData>(API_KEYS.FETCH_ACTIVE_SUBSCRIPTION as any, {}),
{
onSuccess(data) {
if (data && data.meta) {
setPlanMeta({
AUTOMATIC_IMPORTS: data.meta.AUTOMATIC_IMPORTS,
IMAGE_UPLOAD: data.meta.IMAGE_UPLOAD,
IMPORTED_ROWS: data.meta.IMPORTED_ROWS,
REMOVE_BRANDING: data.meta.REMOVE_BRANDING,
});
}
},
enabled: !!email,
}
);

return {
activePlanDetails,
meta,
isActivePlanLoading,
};
}
Loading

0 comments on commit 37c5df0

Please sign in to comment.