diff --git a/.env.example b/.env.example index cfdbdc290..c6f1c0cab 100644 --- a/.env.example +++ b/.env.example @@ -27,6 +27,7 @@ POSTGRES_USER=my_user POSTGRES_DB=panora_db POSTGRES_HOST=postgres + # Each Provider is of form PROVIDER_TICKETING_SOFTWAREMODE_ATTRIBUTE # ================================================ # Integration Providers diff --git a/apps/client-ts/src/app/(Dashboard)/api-keys/page.tsx b/apps/client-ts/src/app/(Dashboard)/api-keys/page.tsx index 12fd14c88..370605b82 100644 --- a/apps/client-ts/src/app/(Dashboard)/api-keys/page.tsx +++ b/apps/client-ts/src/app/(Dashboard)/api-keys/page.tsx @@ -57,7 +57,7 @@ export default function Page() { const {idProject} = useProjectStore(); const {profile} = useProfileStore(); - const { data: apiKeys, isLoading, error } = useApiKeys(idProject); + const { data: apiKeys, isLoading, error } = useApiKeys(); const { mutate } = useApiKeyMutation(); useEffect(() => { diff --git a/apps/client-ts/src/app/(Dashboard)/b2c/profile/page.tsx b/apps/client-ts/src/app/(Dashboard)/b2c/profile/page.tsx index a3c9f2f6b..1d73e54ad 100644 --- a/apps/client-ts/src/app/(Dashboard)/b2c/profile/page.tsx +++ b/apps/client-ts/src/app/(Dashboard)/b2c/profile/page.tsx @@ -19,8 +19,8 @@ import { useQueryClient } from '@tanstack/react-query'; const Profile = () => { - const {profile,setProfile} = useProfileStore(); - const { idProject, setIdProject } = useProjectStore(); + const { profile, setProfile } = useProfileStore(); + const { setIdProject } = useProjectStore(); const queryClient = useQueryClient(); diff --git a/apps/client-ts/src/app/(Dashboard)/configuration/page.tsx b/apps/client-ts/src/app/(Dashboard)/configuration/page.tsx index 77afcc08d..d365165cf 100644 --- a/apps/client-ts/src/app/(Dashboard)/configuration/page.tsx +++ b/apps/client-ts/src/app/(Dashboard)/configuration/page.tsx @@ -46,9 +46,9 @@ import { Heading } from "@/components/ui/heading"; export default function Page() { const {idProject} = useProjectStore(); - const { data: linkedUsers, isLoading, error } = useLinkedUsers(idProject); - const { data: webhooks, isLoading: isWebhooksLoading, error: isWebhooksError } = useWebhooks(idProject); - const {data: ConnectionStrategies, isLoading: isConnectionStrategiesLoading,error: isConnectionStategiesError} = useConnectionStrategies(idProject) + const { data: linkedUsers, isLoading, error } = useLinkedUsers(); + const { data: webhooks, isLoading: isWebhooksLoading, error: isWebhooksError } = useWebhooks(); + const {data: ConnectionStrategies, isLoading: isConnectionStrategiesLoading,error: isConnectionStategiesError} = useConnectionStrategies() const { data: mappings, isLoading: isFieldMappingsLoading, error: isFieldMappingsError } = useFieldMappings(); const [open, setOpen] = useState(false); diff --git a/apps/client-ts/src/app/(Dashboard)/layout.tsx b/apps/client-ts/src/app/(Dashboard)/layout.tsx index cf01b3cf5..195d4fc98 100644 --- a/apps/client-ts/src/app/(Dashboard)/layout.tsx +++ b/apps/client-ts/src/app/(Dashboard)/layout.tsx @@ -7,8 +7,6 @@ import { useEffect, useState } from "react"; import Cookies from 'js-cookie'; import useFetchUserMutation from "@/hooks/mutations/useFetchUserMutation"; - - const inter = Inter({ subsets: ["latin"] }); export default function Layout({ diff --git a/apps/client-ts/src/components/Configuration/AddAuthCredentialsForm.tsx b/apps/client-ts/src/components/Configuration/AddAuthCredentialsForm.tsx index abaa26d4c..99f49ba26 100644 --- a/apps/client-ts/src/components/Configuration/AddAuthCredentialsForm.tsx +++ b/apps/client-ts/src/components/Configuration/AddAuthCredentialsForm.tsx @@ -359,7 +359,6 @@ const AddAuthCredentialsForm = (prop : propType) => {
- {/*
*/} {
- ))} - + ))} + - - - {/* - This is the language that will be used in the dashboard. - */} - + )} - /> + /> {/*
*/} diff --git a/apps/client-ts/src/components/Configuration/FieldMappingModal.tsx b/apps/client-ts/src/components/Configuration/FieldMappingModal.tsx index 59b1fbeec..a13c0090b 100644 --- a/apps/client-ts/src/components/Configuration/FieldMappingModal.tsx +++ b/apps/client-ts/src/components/Configuration/FieldMappingModal.tsx @@ -109,7 +109,7 @@ export function FModal({ onClose }: {onClose: () => void}) { const { data: mappings } = useFieldMappings(); const { mutate: mutateDefineField } = useDefineFieldMutation(); const { mutate: mutateMapField } = useMapFieldMutation(); - const { data: linkedUsers } = useLinkedUsers(idProject); + const { data: linkedUsers } = useLinkedUsers(); const { data: sourceCustomFields, error, isLoading } = useProviderProperties(linkedUserId,sourceProvider); const posthog = usePostHog() diff --git a/apps/client-ts/src/components/Connection/ConnectionTable.tsx b/apps/client-ts/src/components/Connection/ConnectionTable.tsx index bd75c50ec..a271e762e 100644 --- a/apps/client-ts/src/components/Connection/ConnectionTable.tsx +++ b/apps/client-ts/src/components/Connection/ConnectionTable.tsx @@ -26,7 +26,7 @@ import Cookies from 'js-cookie'; export default function ConnectionTable() { const {idProject} = useProjectStore(); - const { data: connections, isLoading, error } = useConnections(idProject); + const { data: connections, isLoading, error } = useConnections(); const [isGenerated, setIsGenerated] = useState(false); const posthog = usePostHog() diff --git a/apps/client-ts/src/components/Events/EventsTable.tsx b/apps/client-ts/src/components/Events/EventsTable.tsx index 5fc3285ff..b60993520 100644 --- a/apps/client-ts/src/components/Events/EventsTable.tsx +++ b/apps/client-ts/src/components/Events/EventsTable.tsx @@ -21,7 +21,7 @@ export default function EventsTable() { } = useEvents({ page: pagination.page, pageSize: pagination.pageSize, - }, idProject); + }); //TODO const transformedEvents = events?.map((event: Event) => ({ diff --git a/apps/client-ts/src/components/RootLayout/index.tsx b/apps/client-ts/src/components/RootLayout/index.tsx index f2c254d34..f5cacc40e 100644 --- a/apps/client-ts/src/components/RootLayout/index.tsx +++ b/apps/client-ts/src/components/RootLayout/index.tsx @@ -12,19 +12,14 @@ import useProfileStore from '@/state/profileStore'; import useProjectStore from '@/state/projectStore'; import { ThemeToggle } from '@/components/Nav/theme-toggle'; import useProjects from '@/hooks/useProjects'; +import useRefreshAccessTokenMutation from '@/hooks/mutations/useRefreshAccessTokenMutation'; export const RootLayout = ({children}:{children:React.ReactNode}) => { const router = useRouter() const base = process.env.NEXT_PUBLIC_WEBAPP_DOMAIN; - - - const {profile} = useProfileStore() const {data : projectsData} = useProjects(); const { idProject, setIdProject } = useProjectStore(); - - - // const { setIdProject } = useProjectStore(); - + const {mutate : refreshAccessToken} = useRefreshAccessTokenMutation() useEffect(() => { if(projectsData) @@ -34,10 +29,9 @@ export const RootLayout = ({children}:{children:React.ReactNode}) => { { console.log("Project Id setting : ",projectsData[0]?.id_project) setIdProject(projectsData[0]?.id_project); - } } - },[projectsData]) + },[idProject, projectsData, refreshAccessToken, setIdProject]) const handlePageChange = (page: string) => { if (page) { diff --git a/apps/client-ts/src/components/shared/data-table-row-actions.tsx b/apps/client-ts/src/components/shared/data-table-row-actions.tsx index fbd98aede..b9f7b2075 100644 --- a/apps/client-ts/src/components/shared/data-table-row-actions.tsx +++ b/apps/client-ts/src/components/shared/data-table-row-actions.tsx @@ -42,7 +42,6 @@ export function DataTableRowActions({ Edit Make a copy Favorite - {row.id} Labels diff --git a/apps/client-ts/src/components/shared/team-switcher.tsx b/apps/client-ts/src/components/shared/team-switcher.tsx index b4e7d5362..6203a62ff 100644 --- a/apps/client-ts/src/components/shared/team-switcher.tsx +++ b/apps/client-ts/src/components/shared/team-switcher.tsx @@ -56,6 +56,7 @@ import config from "@/lib/config" import { Skeleton } from "@/components/ui/skeleton"; import useProfileStore from "@/state/profileStore" import { projects as Project } from 'api'; +import useRefreshAccessTokenMutation from "@/hooks/mutations/useRefreshAccessTokenMutation" const projectFormSchema = z.object({ @@ -85,15 +86,7 @@ export default function TeamSwitcher({ className ,projects}: TeamSwitcherProps) const { profile } = useProfileStore(); const { idProject, setIdProject } = useProjectStore(); - - // const projects = profile?.projects; - - // useEffect(() => { - // if(idProject==="" && projects) - // { - // setIdProject(projects[0]?.id_project) - // } - // },[projects]) + const {mutate : refreshAccessToken} = useRefreshAccessTokenMutation() const handleOpenChange = (open: boolean) => { setShowNewDialog(prevState => ({ ...prevState, open })); @@ -149,6 +142,7 @@ export default function TeamSwitcher({ className ,projects}: TeamSwitcherProps) key={project.id_project} onSelect={() => { setIdProject(project.id_project) + refreshAccessToken(project.id_project) setOpen(false) }} className="text-sm" diff --git a/apps/client-ts/src/hooks/mutations/useLoginMutation.tsx b/apps/client-ts/src/hooks/mutations/useLoginMutation.tsx index 5939dd8dc..39897ca01 100644 --- a/apps/client-ts/src/hooks/mutations/useLoginMutation.tsx +++ b/apps/client-ts/src/hooks/mutations/useLoginMutation.tsx @@ -1,8 +1,7 @@ import config from '@/lib/config'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; import { toast } from "sonner" import useProfileStore from '@/state/profileStore'; -import { projects as Project } from 'api'; import Cookies from 'js-cookie'; type IUserDto = { @@ -18,15 +17,11 @@ interface ILoginInputDto { password_hash:string } - - interface ILoginOutputDto { user: IUserDto, access_token: string - } - const useLoginMutation = () => { const {setProfile} = useProfileStore() @@ -41,15 +36,10 @@ const useLoginMutation = () => { }, }); - - if (!response.ok) { throw new Error("Login Failed!!") } - - - return response.json(); }; return useMutation({ @@ -73,10 +63,9 @@ const useLoginMutation = () => { }) }, onSuccess: (data : ILoginOutputDto) => { - setProfile(data.user); Cookies.set('access_token',data.access_token,{expires:1}); - console.log("Bearer Token in client Side : ",data.access_token); + //console.log("Bearer Token in client Side : ",data.access_token); toast.success("User has been generated !", { description: "", diff --git a/apps/client-ts/src/hooks/mutations/useRefreshAccessTokenMutation.tsx b/apps/client-ts/src/hooks/mutations/useRefreshAccessTokenMutation.tsx new file mode 100644 index 000000000..63e87fce7 --- /dev/null +++ b/apps/client-ts/src/hooks/mutations/useRefreshAccessTokenMutation.tsx @@ -0,0 +1,58 @@ +import config from '@/lib/config'; +import { useMutation } from '@tanstack/react-query'; +import { toast } from "sonner" +import Cookies from 'js-cookie'; + +interface IRefreshOutputDto { + access_token: string +} + +const useRefreshAccessTokenMutation = () => { + const refreshAccessToken = async (projectId: string) => { + const response = await fetch(`${config.API_URL}/auth/refresh-token`, { + method: 'POST', + body: JSON.stringify({ + projectId: projectId + }), + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${Cookies.get('access_token')}`, + }, + }); + + if (!response.ok) { + throw new Error("Login Failed!!") + } + + return response.json(); + }; + return useMutation({ + mutationFn: refreshAccessToken, + onMutate: () => { + }, + onError: (error) => { + toast.error("Refreshing token generation failed !", { + description: error as any, + action: { + label: "Close", + onClick: () => console.log("Close"), + }, + }) + }, + onSuccess: (data : IRefreshOutputDto) => { + Cookies.remove('access_token'); + Cookies.set('access_token', data.access_token, {expires:1}); + /*toast.success("Refresh has been generated !", { + description: "", + action: { + label: "Close", + onClick: () => console.log("Close"), + }, + })*/ + }, + onSettled: () => { + }, + }); +}; + +export default useRefreshAccessTokenMutation; diff --git a/apps/client-ts/src/hooks/useApiKeys.tsx b/apps/client-ts/src/hooks/useApiKeys.tsx index e814d2ef2..6bd57e3af 100644 --- a/apps/client-ts/src/hooks/useApiKeys.tsx +++ b/apps/client-ts/src/hooks/useApiKeys.tsx @@ -3,11 +3,11 @@ import { useQuery } from '@tanstack/react-query'; import { api_keys as ApiKey } from 'api'; import Cookies from 'js-cookie'; -const useApiKeys = (project_id: string) => { +const useApiKeys = () => { return useQuery({ queryKey: ['api-keys'], queryFn: async (): Promise => { - const response = await fetch(`${config.API_URL}/auth/api-keys?project_id=${project_id}`, + const response = await fetch(`${config.API_URL}/auth/api-keys`, { method: 'GET', headers: { diff --git a/apps/client-ts/src/hooks/useConnectionStrategies.tsx b/apps/client-ts/src/hooks/useConnectionStrategies.tsx index 3adfc0899..8adca921f 100644 --- a/apps/client-ts/src/hooks/useConnectionStrategies.tsx +++ b/apps/client-ts/src/hooks/useConnectionStrategies.tsx @@ -3,11 +3,11 @@ import { useQuery } from '@tanstack/react-query'; import { connection_strategies as ConnectionStrategies } from 'api'; import Cookies from 'js-cookie'; -const useConnectionStrategies = (projectId : string) => { +const useConnectionStrategies = () => { return useQuery({ queryKey: ['connection-strategies'], queryFn: async (): Promise => { - const response = await fetch(`${config.API_URL}/connections-strategies/getConnectionStrategiesForProject?projectId=${projectId}`, + const response = await fetch(`${config.API_URL}/connections-strategies/getConnectionStrategiesForProject`, { method: 'GET', headers: { diff --git a/apps/client-ts/src/hooks/useConnections.tsx b/apps/client-ts/src/hooks/useConnections.tsx index 15e6f607a..166936958 100644 --- a/apps/client-ts/src/hooks/useConnections.tsx +++ b/apps/client-ts/src/hooks/useConnections.tsx @@ -4,11 +4,11 @@ import { connections as Connection } from 'api'; import Cookies from 'js-cookie'; -const useConnections = (project_id: string) => { +const useConnections = () => { return useQuery({ queryKey: ['connections'], queryFn: async (): Promise => { - const response = await fetch(`${config.API_URL}/connections?project_id=${project_id}`,{ + const response = await fetch(`${config.API_URL}/connections`,{ method: 'GET', headers: { 'Content-Type': 'application/json', diff --git a/apps/client-ts/src/hooks/useEvents.tsx b/apps/client-ts/src/hooks/useEvents.tsx index 84f02c130..3f726d02b 100644 --- a/apps/client-ts/src/hooks/useEvents.tsx +++ b/apps/client-ts/src/hooks/useEvents.tsx @@ -4,13 +4,13 @@ import { useQuery } from '@tanstack/react-query'; import { events as Event } from 'api'; import Cookies from 'js-cookie'; -const fetchEvents = async (params: PaginationParams, project_id: string): Promise => { +const fetchEvents = async (params: PaginationParams): Promise => { const searchParams = new URLSearchParams({ page: params.page.toString(), pageSize: params.pageSize.toString(), }); - const response = await fetch(`${config.API_URL}/events?project_id=${project_id}&${searchParams.toString()}`, + const response = await fetch(`${config.API_URL}/events?${searchParams.toString()}`, { method: 'GET', headers: { @@ -25,10 +25,10 @@ const fetchEvents = async (params: PaginationParams, project_id: string): Promis return response.json(); }; -const useEvents = (params: PaginationParams, project_id: string) => { +const useEvents = (params: PaginationParams) => { return useQuery({ queryKey: ['events', { page: params.page, pageSize: params.pageSize }], - queryFn: () => fetchEvents(params, project_id), + queryFn: () => fetchEvents(params), }); }; diff --git a/apps/client-ts/src/hooks/useLinkedUsers.tsx b/apps/client-ts/src/hooks/useLinkedUsers.tsx index 54a062044..484b9575b 100644 --- a/apps/client-ts/src/hooks/useLinkedUsers.tsx +++ b/apps/client-ts/src/hooks/useLinkedUsers.tsx @@ -3,11 +3,11 @@ import { useQuery } from '@tanstack/react-query'; import { linked_users as LinkedUser } from 'api'; import Cookies from 'js-cookie'; -const useLinkedUsers = (project_id: string) => { +const useLinkedUsers = () => { return useQuery({ queryKey: ['linked-users'], queryFn: async (): Promise => { - const response = await fetch(`${config.API_URL}/linked-users?project_id=${project_id}`, + const response = await fetch(`${config.API_URL}/linked-users`, { method: 'GET', headers: { diff --git a/apps/client-ts/src/hooks/useWebhooks.tsx b/apps/client-ts/src/hooks/useWebhooks.tsx index 636c7172f..482580a96 100644 --- a/apps/client-ts/src/hooks/useWebhooks.tsx +++ b/apps/client-ts/src/hooks/useWebhooks.tsx @@ -3,12 +3,12 @@ import { useQuery } from '@tanstack/react-query'; import { webhook_endpoints as Webhook } from 'api'; import Cookies from 'js-cookie'; -const useWebhooks = (project_id: string) => { +const useWebhooks = () => { return useQuery({ queryKey: ['webhooks'], queryFn: async (): Promise => { console.log("Webhook mutation called") - const response = await fetch(`${config.API_URL}/webhook?project_id=${project_id}`, + const response = await fetch(`${config.API_URL}/webhook`, { method: 'GET', headers: { diff --git a/apps/magic-link/src/App.tsx b/apps/magic-link/src/App.tsx index d4037e076..bb4967ee6 100644 --- a/apps/magic-link/src/App.tsx +++ b/apps/magic-link/src/App.tsx @@ -1,16 +1,7 @@ -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import './App.css' import ProviderModal from './lib/ProviderModal' -import { PanoraSDK } from "@panora/typescript-sdk"; function App() { - const sdk = new PanoraSDK({accessToken: 'YOUR_ACCESS_TOKEN'}); - (async () => { - const result = await sdk.main - .appControllerGetHello(); - console.log(result); - })(); - return (
diff --git a/packages/api/package.json b/packages/api/package.json index 00958abad..e15ec2531 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -39,6 +39,7 @@ "@nestjs/swagger": "^7.1.14", "@nestjs/throttler": "^5.1.1", "@ntegral/nestjs-sentry": "^4.0.0", + "@panora/shared": "workspace:*", "@prisma/client": "^5.4.2", "@sentry/node": "^7.80.0", "@sentry/tracing": "^7.80.0", @@ -46,7 +47,7 @@ "bcrypt": "^5.1.1", "bull": "^4.11.5", "class-transformer": "^0.5.1", - "class-validator": "^0.14.0", + "class-validator": "^0.14.1", "cookie-parser": "^1.4.6", "cors": "^2.8.5", "crypto": "^1.0.1", @@ -54,6 +55,7 @@ "install": "^0.13.0", "js-yaml": "^4.1.0", "nestjs-pino": "^3.5.0", + "openai": "^4.38.5", "passport": "^0.6.0", "passport-headerapikey": "^1.2.2", "passport-jwt": "^4.0.1", @@ -63,8 +65,7 @@ "rxjs": "^7.8.1", "stytch": "^10.5.0", "uuid": "^9.0.1", - "yargs": "^17.7.2", - "@panora/shared": "workspace:*" + "yargs": "^17.7.2" }, "devDependencies": { "@nestjs/cli": "^10.0.0", diff --git a/packages/api/src/@core/auth/auth.controller.ts b/packages/api/src/@core/auth/auth.controller.ts index 6f34af10f..acfda5f99 100644 --- a/packages/api/src/@core/auth/auth.controller.ts +++ b/packages/api/src/@core/auth/auth.controller.ts @@ -14,7 +14,7 @@ import { LoggerService } from '@@core/logger/logger.service'; import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiKeyDto } from './dto/api-key.dto'; import { LoginDto } from './dto/login.dto'; -import { ValidateUserGuard } from '@@core/utils/guards/validate-user.guard'; +import { RefreshDto } from './dto/refresh.dto'; @ApiTags('auth') @Controller('auth') @@ -67,14 +67,11 @@ export class AuthController { @ApiOperation({ operationId: 'getApiKeys', summary: 'Retrieve API Keys' }) @ApiResponse({ status: 200 }) - @UseGuards(JwtAuthGuard, ValidateUserGuard) + @UseGuards(JwtAuthGuard) @Get('api-keys') - async getApiKeys( - @Request() req: any, - @Query('project_id') project_id: string, - ) { - const id_user = req.user.id_user; // Extracted from JWT payload - return this.authService.getApiKeys(id_user, project_id); + async getApiKeys(@Request() req: any) { + const { id_project } = req.user; + return this.authService.getApiKeys(id_project); } @ApiOperation({ operationId: 'generateApiKey', summary: 'Create API Key' }) @@ -89,4 +86,24 @@ export class AuthController { data.keyName, ); } + + @ApiOperation({ + operationId: 'refreshAccessToken', + summary: 'Refresh Access Token', + }) + @ApiBody({ type: RefreshDto }) + @ApiResponse({ status: 201 }) + @UseGuards(JwtAuthGuard) + @Post('refresh-token') + refreshAccessToken(@Request() req: any, @Body() body: RefreshDto) { + const { projectId } = body; + const { id_user, email, first_name, last_name } = req.user; + return this.authService.refreshAccessToken( + projectId, + id_user, + email, + first_name, + last_name, + ); + } } diff --git a/packages/api/src/@core/auth/auth.service.ts b/packages/api/src/@core/auth/auth.service.ts index 30a89aa9e..c495cf585 100644 --- a/packages/api/src/@core/auth/auth.service.ts +++ b/packages/api/src/@core/auth/auth.service.ts @@ -60,11 +60,10 @@ export class AuthService { } } - async getApiKeys(user_id: string, project_id: string) { + async getApiKeys(project_id: string) { try { return await this.prisma.api_keys.findMany({ where: { - id_user: user_id, id_project: project_id, }, }); @@ -141,6 +140,12 @@ export class AuthService { }, }); + const project = await this.prisma.projects.findFirst({ + where: { + id_user: foundUser.id_user, + }, + }); + if (!foundUser) { throw new UnauthorizedException('user does not exist!'); } @@ -159,6 +164,7 @@ export class AuthService { sub: userData.id_user, first_name: userData.first_name, last_name: userData.last_name, + id_project: project.id_project, }; return { @@ -177,6 +183,31 @@ export class AuthService { } } + async refreshAccessToken( + projectId: string, + id_user: string, + email: string, + first_name: string, + last_name: string, + ) { + try { + const payload = { + email: email, + sub: id_user, + first_name: first_name, + last_name: last_name, + id_project: projectId, + }; + return { + access_token: this.jwtService.sign(payload, { + secret: process.env.JWT_SECRET, + }), + }; + } catch (error) { + handleServiceError(error, this.logger); + } + } + hashApiKey(apiKey: string): string { return crypto.createHash('sha256').update(apiKey).digest('hex'); } diff --git a/packages/api/src/@core/auth/dto/refresh.dto.ts b/packages/api/src/@core/auth/dto/refresh.dto.ts new file mode 100644 index 000000000..6492650dd --- /dev/null +++ b/packages/api/src/@core/auth/dto/refresh.dto.ts @@ -0,0 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class RefreshDto { + @ApiProperty() + projectId: string; +} diff --git a/packages/api/src/@core/auth/strategies/jwt.strategy.ts b/packages/api/src/@core/auth/strategies/jwt.strategy.ts index c75ff6529..6c3ea8926 100644 --- a/packages/api/src/@core/auth/strategies/jwt.strategy.ts +++ b/packages/api/src/@core/auth/strategies/jwt.strategy.ts @@ -20,6 +20,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { email: payload.email, first_name: payload.first_name, last_name: payload.last_name, + id_project: payload.id_project, }; } } diff --git a/packages/api/src/@core/connections-strategies/connections-strategies.controller.ts b/packages/api/src/@core/connections-strategies/connections-strategies.controller.ts index 68eb39b6b..e3ed06180 100644 --- a/packages/api/src/@core/connections-strategies/connections-strategies.controller.ts +++ b/packages/api/src/@core/connections-strategies/connections-strategies.controller.ts @@ -1,4 +1,12 @@ -import { Body, Controller, Get, Post, Query, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + Get, + Post, + Query, + UseGuards, + Request, +} from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ConnectionsStrategiesService } from './connections-strategies.service'; @@ -8,7 +16,6 @@ import { DeleteCSDto } from './dto/delete-cs.dto'; import { UpdateCSDto } from './dto/update-cs.dto'; import { ConnectionStrategyCredentials } from './dto/get-connection-cs-credentials.dto'; import { JwtAuthGuard } from '@@core/auth/guards/jwt-auth.guard'; -import { ValidateUserGuard } from '@@core/utils/guards/validate-user.guard'; @ApiTags('connections-strategies') @Controller('connections-strategies') @@ -32,7 +39,6 @@ export class ConnectionsStrategiesController { @Body() connectionStrategyCreateDto: CreateConnectionStrategyDto, ) { const { projectId, type, attributes, values } = connectionStrategyCreateDto; - // validate user against project_id return await this.connectionsStrategiesService.createConnectionStrategy( projectId, type, @@ -50,7 +56,6 @@ export class ConnectionsStrategiesController { @UseGuards(JwtAuthGuard) @Post('toggle') async toggleConnectionStrategy(@Body() data: ToggleStrategyDto) { - // validate user against project_id return await this.connectionsStrategiesService.toggle(data.id_cs); } @@ -63,7 +68,6 @@ export class ConnectionsStrategiesController { @UseGuards(JwtAuthGuard) @Post('delete') async deleteConnectionStrategy(@Body() data: DeleteCSDto) { - // validate user against project_id return await this.connectionsStrategiesService.deleteConnectionStrategy( data.id, ); @@ -131,13 +135,12 @@ export class ConnectionsStrategiesController { summary: 'Fetch All Connection Strategies for Project', }) @ApiResponse({ status: 200 }) - @UseGuards(JwtAuthGuard, ValidateUserGuard) + @UseGuards(JwtAuthGuard) @Get('getConnectionStrategiesForProject') - async getConnectionStrategiesForProject( - @Query('projectId') projectId: string, - ) { + async getConnectionStrategiesForProject(@Request() req: any) { + const { id_project } = req.user; return await this.connectionsStrategiesService.getConnectionStrategiesForProject( - projectId, + id_project, ); } diff --git a/packages/api/src/@core/connections/connections.controller.ts b/packages/api/src/@core/connections/connections.controller.ts index 84be76817..410908a50 100644 --- a/packages/api/src/@core/connections/connections.controller.ts +++ b/packages/api/src/@core/connections/connections.controller.ts @@ -18,7 +18,6 @@ import { ProviderVertical } from '@panora/shared'; import { AccountingConnectionsService } from './accounting/services/accounting.connection.service'; import { MarketingAutomationConnectionsService } from './marketingautomation/services/marketingautomation.connection.service'; import { JwtAuthGuard } from '@@core/auth/guards/jwt-auth.guard'; -import { ValidateUserGuard } from '@@core/utils/guards/validate-user.guard'; export type StateDataType = { projectId: string; @@ -111,8 +110,6 @@ export class ConnectionsController { code, ); break; - case ProviderVertical.Unknown: - break; } res.redirect(returnUrl); } catch (error) { @@ -145,15 +142,14 @@ export class ConnectionsController { summary: 'List Connections', }) @ApiResponse({ status: 200 }) - @UseGuards(JwtAuthGuard, ValidateUserGuard) + @UseGuards(JwtAuthGuard) @Get() - async getConnections( - @Request() req: any, - @Query('projectId') projectId: string, - ) { + async getConnections(@Request() req: any) { + const { id_project } = req.user; + console.log('Req data is:', req.user); return await this.prisma.connections.findMany({ where: { - id_project: projectId, + id_project: id_project, }, }); } diff --git a/packages/api/src/@core/core.module.ts b/packages/api/src/@core/core.module.ts index 8a6c04a3b..cb2233734 100644 --- a/packages/api/src/@core/core.module.ts +++ b/packages/api/src/@core/core.module.ts @@ -12,6 +12,7 @@ import { WebhookModule } from './webhook/webhook.module'; import { EnvironmentModule } from './environment/environment.module'; import { EncryptionService } from './encryption/encryption.service'; import { ConnectionsStrategiesModule } from './connections-strategies/connections-strategies.module'; +import { SyncModule } from './sync/sync.module'; @Module({ imports: [ @@ -27,6 +28,7 @@ import { ConnectionsStrategiesModule } from './connections-strategies/connection WebhookModule, EnvironmentModule, ConnectionsStrategiesModule, + SyncModule, ], exports: [ AuthModule, @@ -41,6 +43,7 @@ import { ConnectionsStrategiesModule } from './connections-strategies/connection WebhookModule, EnvironmentModule, ConnectionsStrategiesModule, + SyncModule, ], providers: [EncryptionService], }) diff --git a/packages/api/src/@core/events/events.controller.ts b/packages/api/src/@core/events/events.controller.ts index 758410fa9..c8704c6bb 100644 --- a/packages/api/src/@core/events/events.controller.ts +++ b/packages/api/src/@core/events/events.controller.ts @@ -5,13 +5,13 @@ import { UseGuards, UsePipes, ValidationPipe, + Request, } from '@nestjs/common'; import { EventsService } from './events.service'; import { LoggerService } from '@@core/logger/logger.service'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { PaginationDto } from '@@core/utils/dtos/pagination.dto'; import { JwtAuthGuard } from '@@core/auth/guards/jwt-auth.guard'; -import { ValidateUserGuard } from '@@core/utils/guards/validate-user.guard'; @ApiTags('events') @Controller('events') @@ -34,13 +34,11 @@ export class EventsController { }, }), ) - @UseGuards(JwtAuthGuard, ValidateUserGuard) + @UseGuards(JwtAuthGuard) @Get() - async getEvents( - @Query() dto: PaginationDto, - @Query('project_id') project_id: string, - ) { - return await this.eventsService.findEvents(dto, project_id); + async getEvents(@Query() dto: PaginationDto, @Request() req: any) { + const { id_project } = req.user; + return await this.eventsService.findEvents(dto, id_project); } // todo diff --git a/packages/api/src/@core/linked-users/linked-users.controller.ts b/packages/api/src/@core/linked-users/linked-users.controller.ts index acd605dc3..94a585f66 100644 --- a/packages/api/src/@core/linked-users/linked-users.controller.ts +++ b/packages/api/src/@core/linked-users/linked-users.controller.ts @@ -1,4 +1,12 @@ -import { Body, Controller, Get, Post, Query, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + Get, + Post, + Query, + UseGuards, + Request, +} from '@nestjs/common'; import { LinkedUsersService } from './linked-users.service'; import { LoggerService } from '../logger/logger.service'; import { CreateLinkedUserDto } from './dto/create-linked-user.dto'; @@ -10,7 +18,6 @@ import { ApiTags, } from '@nestjs/swagger'; import { JwtAuthGuard } from '@@core/auth/guards/jwt-auth.guard'; -import { ValidateUserGuard } from '@@core/utils/guards/validate-user.guard'; @ApiTags('linked-users') @Controller('linked-users') @@ -37,10 +44,11 @@ export class LinkedUsersController { summary: 'Retrieve Linked Users', }) @ApiResponse({ status: 200 }) - @UseGuards(JwtAuthGuard, ValidateUserGuard) + @UseGuards(JwtAuthGuard) @Get() - getLinkedUsers(@Query('project_id') project_id: string) { - return this.linkedUsersService.getLinkedUsers(project_id); + getLinkedUsers(@Request() req: any) { + const { id_project } = req.user; + return this.linkedUsersService.getLinkedUsers(id_project); } @ApiOperation({ diff --git a/packages/api/src/@core/sync/sync.controller.ts b/packages/api/src/@core/sync/sync.controller.ts new file mode 100644 index 000000000..7bd7a0d6c --- /dev/null +++ b/packages/api/src/@core/sync/sync.controller.ts @@ -0,0 +1,40 @@ +import { Controller, Get, Param, UseGuards } from '@nestjs/common'; +import { CoreSyncService } from './sync.service'; +import { LoggerService } from '../logger/logger.service'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; + +@ApiTags('sync') +@Controller('sync') +export class SyncController { + constructor( + private readonly syncService: CoreSyncService, + private logger: LoggerService, + ) { + this.logger.setContext(SyncController.name); + } + + @ApiOperation({ + operationId: 'getSyncStatus', + summary: 'Retrieve sync status of a certain vertical', + }) + @ApiResponse({ status: 200 }) + @Get('status/:vertical') + getSyncStatus(@Param('vertical') vertical: string) { + return this.syncService.getSyncStatus(vertical); + } + + //this route must be protected for premium users (regular sync is one every 24 hours) + @ApiOperation({ + operationId: 'resync', + summary: 'Resync common objects across a vertical', + }) + @ApiResponse({ status: 200 }) + @UseGuards(ApiKeyAuthGuard) + @Get('resync/:vertical') + resync(@Param('vertical') vertical: string) { + // TODO: get the right user_id of the premium user using the api key + const user_id = ''; + return this.syncService.resync(vertical, user_id); + } +} diff --git a/packages/api/src/@core/sync/sync.module.ts b/packages/api/src/@core/sync/sync.module.ts new file mode 100644 index 000000000..e8d5e682d --- /dev/null +++ b/packages/api/src/@core/sync/sync.module.ts @@ -0,0 +1,86 @@ +import { Module } from '@nestjs/common'; +import { CoreSyncService } from './sync.service'; +import { SyncController } from './sync.controller'; +import { LoggerService } from '../logger/logger.service'; +import { PrismaService } from '../prisma/prisma.service'; +import { SyncService as CrmCompanySyncService } from '@crm/company/sync/sync.service'; +import { SyncService as CrmContactSyncService } from '@crm/contact/sync/sync.service'; +import { SyncService as CrmDealSyncService } from '@crm/deal/sync/sync.service'; +import { SyncService as CrmEngagementSyncService } from '@crm/engagement/sync/sync.service'; +import { SyncService as CrmNoteSyncService } from '@crm/note/sync/sync.service'; +import { SyncService as CrmStageSyncService } from '@crm/stage/sync/sync.service'; +import { SyncService as CrmTaskSyncService } from '@crm/task/sync/sync.service'; +import { SyncService as CrmUserSyncService } from '@crm/user/sync/sync.service'; +import { SyncService as TicketingAccountSyncService } from '@ticketing/account/sync/sync.service'; +import { SyncService as TicketingCollectionSyncService } from '@ticketing/collection/sync/sync.service'; +import { SyncService as TicketingCommentSyncService } from '@ticketing/comment/sync/sync.service'; +import { SyncService as TicketingContactSyncService } from '@ticketing/contact/sync/sync.service'; +import { SyncService as TicketingTagSyncService } from '@ticketing/tag/sync/sync.service'; +import { SyncService as TicketingTeamSyncService } from '@ticketing/team/sync/sync.service'; +import { SyncService as TicketingTicketSyncService } from '@ticketing/ticket/sync/sync.service'; +import { SyncService as TicketingUserSyncService } from '@ticketing/user/sync/sync.service'; +import { BullModule } from '@nestjs/bull'; +import { CompanyModule } from '@crm/company/company.module'; +import { ContactModule } from '@crm/contact/contact.module'; +import { DealModule } from '@crm/deal/deal.module'; +import { EngagementModule } from '@crm/engagement/engagement.module'; +import { NoteModule } from '@crm/note/note.module'; +import { StageModule } from '@crm/stage/stage.module'; +import { TaskModule } from '@crm/task/task.module'; +import { UserModule } from '@crm/user/user.module'; +import { AccountModule } from '@ticketing/account/account.module'; +import { CollectionModule } from '@ticketing/collection/collection.module'; +import { CommentModule } from '@ticketing/comment/comment.module'; +import { ContactModule as TContactModule } from '@ticketing/contact/contact.module'; +import { TagModule } from '@ticketing/tag/tag.module'; +import { TeamModule } from '@ticketing/team/team.module'; +import { TicketModule } from '@ticketing/ticket/ticket.module'; +import { UserModule as TUserModule } from '@ticketing/user/user.module'; + +@Module({ + imports: [ + BullModule.registerQueue( + { name: 'webhookDelivery' }, + { name: 'syncTasks' }, + ), + CompanyModule, + ContactModule, + DealModule, + EngagementModule, + NoteModule, + StageModule, + TaskModule, + UserModule, + AccountModule, + CollectionModule, + CommentModule, + TContactModule, + TagModule, + TeamModule, + TicketModule, + TUserModule, + ], + providers: [ + CoreSyncService, + LoggerService, + PrismaService, + CrmCompanySyncService, + CrmContactSyncService, + CrmDealSyncService, + CrmEngagementSyncService, + CrmNoteSyncService, + CrmStageSyncService, + CrmTaskSyncService, + CrmUserSyncService, + TicketingAccountSyncService, + TicketingCollectionSyncService, + TicketingCommentSyncService, + TicketingContactSyncService, + TicketingTagSyncService, + TicketingTeamSyncService, + TicketingTicketSyncService, + TicketingUserSyncService, + ], + controllers: [SyncController], +}) +export class SyncModule {} diff --git a/packages/api/src/@core/sync/sync.service.ts b/packages/api/src/@core/sync/sync.service.ts new file mode 100644 index 000000000..8686107f1 --- /dev/null +++ b/packages/api/src/@core/sync/sync.service.ts @@ -0,0 +1,103 @@ +import { Injectable } from '@nestjs/common'; +import { LoggerService } from '../logger/logger.service'; +import { handleServiceError } from '@@core/utils/errors'; +import { SyncService as CrmCompanySyncService } from '@crm/company/sync/sync.service'; +import { SyncService as CrmContactSyncService } from '@crm/contact/sync/sync.service'; +import { SyncService as CrmDealSyncService } from '@crm/deal/sync/sync.service'; +import { SyncService as CrmEngagementSyncService } from '@crm/engagement/sync/sync.service'; +import { SyncService as CrmNoteSyncService } from '@crm/note/sync/sync.service'; +import { SyncService as CrmStageSyncService } from '@crm/stage/sync/sync.service'; +import { SyncService as CrmTaskSyncService } from '@crm/task/sync/sync.service'; +import { SyncService as CrmUserSyncService } from '@crm/user/sync/sync.service'; +import { SyncService as TicketingAccountSyncService } from '@ticketing/account/sync/sync.service'; +import { SyncService as TicketingCollectionSyncService } from '@ticketing/collection/sync/sync.service'; +import { SyncService as TicketingCommentSyncService } from '@ticketing/comment/sync/sync.service'; +import { SyncService as TicketingContactSyncService } from '@ticketing/contact/sync/sync.service'; +import { SyncService as TicketingTagSyncService } from '@ticketing/tag/sync/sync.service'; +import { SyncService as TicketingTeamSyncService } from '@ticketing/team/sync/sync.service'; +import { SyncService as TicketingTicketSyncService } from '@ticketing/ticket/sync/sync.service'; +import { SyncService as TicketingUserSyncService } from '@ticketing/user/sync/sync.service'; + +@Injectable() +export class CoreSyncService { + constructor( + private logger: LoggerService, + private CrmCompanySyncService: CrmCompanySyncService, + private CrmContactSyncService: CrmContactSyncService, + private CrmDealSyncService: CrmDealSyncService, + private CrmEngagementSyncService: CrmEngagementSyncService, + private CrmNoteSyncService: CrmNoteSyncService, + private CrmStageSyncService: CrmStageSyncService, + private CrmTaskSyncService: CrmTaskSyncService, + private CrmUserSyncService: CrmUserSyncService, + private TicketingAccountSyncService: TicketingAccountSyncService, + private TicketingCollectionSyncService: TicketingCollectionSyncService, + private TicketingCommentSyncService: TicketingCommentSyncService, + private TicketingContactSyncService: TicketingContactSyncService, + private TicketingTagSyncService: TicketingTagSyncService, + private TicketingTeamSyncService: TicketingTeamSyncService, + private TicketingTicketSyncService: TicketingTicketSyncService, + private TicketingUserSyncService: TicketingUserSyncService, + ) { + this.logger.setContext(CoreSyncService.name); + } + + // we must have a sync_jobs table with 7 (verticals) rows, one of each is syncing details + async getSyncStatus(vertical: string) { + try { + } catch (error) { + handleServiceError(error, this.logger); + } + } + + // todo: test behaviour + async resync(vertical: string, user_id: string) { + // premium feature + // trigger a resync for the vertical but only for linked_users who belong to user_id account + const tasks = []; + try { + switch (vertical.toLowerCase()) { + case 'crm': + tasks.push(this.CrmCompanySyncService.syncCompanies(user_id)); + tasks.push(this.CrmContactSyncService.syncContacts(user_id)); + tasks.push(this.CrmDealSyncService.syncDeals(user_id)); + tasks.push(this.CrmEngagementSyncService.syncEngagements(user_id)); + tasks.push(this.CrmNoteSyncService.syncNotes(user_id)); + tasks.push(this.CrmStageSyncService.syncStages(user_id)); + tasks.push(this.CrmTaskSyncService.syncTasks(user_id)); + tasks.push(this.CrmUserSyncService.syncUsers(user_id)); + break; + case 'ticketing': + tasks.push(this.TicketingAccountSyncService.syncAccounts(user_id)); + tasks.push( + this.TicketingCollectionSyncService.syncCollections(user_id), + ); + tasks.push(this.TicketingCommentSyncService.syncComments(user_id)); + tasks.push(this.TicketingContactSyncService.syncContacts(user_id)); + tasks.push(this.TicketingTagSyncService.syncTags(user_id)); + tasks.push(this.TicketingTeamSyncService.syncTeams(user_id)); + tasks.push(this.TicketingTicketSyncService.syncTickets(user_id)); + tasks.push(this.TicketingUserSyncService.syncUsers(user_id)); + break; + } + return { + timestamp: new Date(), + vertical: vertical, + status: `SYNCING`, + }; + } catch (error) { + handleServiceError(error, this.logger); + } finally { + // Handle background tasks completion + Promise.allSettled(tasks).then((results) => { + results.forEach((result, index) => { + if (result.status === 'fulfilled') { + console.log(`Task ${index} completed successfully.`); + } else if (result.status === 'rejected') { + console.log(`Task ${index} failed:`, result.reason); + } + }); + }); + } + } +} diff --git a/packages/api/src/@core/utils/unification/desunify.ts b/packages/api/src/@core/utils/unification/desunify.ts index 0df893332..e6966a803 100644 --- a/packages/api/src/@core/utils/unification/desunify.ts +++ b/packages/api/src/@core/utils/unification/desunify.ts @@ -56,7 +56,5 @@ export async function desunify({ providerName, customFieldMappings, }); - case ProviderVertical.Unknown: - break; } } diff --git a/packages/api/src/@core/utils/unification/unify.ts b/packages/api/src/@core/utils/unification/unify.ts index bbb004379..a37cfa45a 100644 --- a/packages/api/src/@core/utils/unification/unify.ts +++ b/packages/api/src/@core/utils/unification/unify.ts @@ -57,8 +57,6 @@ export async function unify({ providerName, customFieldMappings, }); - case ProviderVertical.Unknown: - break; } return; } diff --git a/packages/api/src/@core/webhook/webhook.controller.ts b/packages/api/src/@core/webhook/webhook.controller.ts index 6a208e04f..499f88ff2 100644 --- a/packages/api/src/@core/webhook/webhook.controller.ts +++ b/packages/api/src/@core/webhook/webhook.controller.ts @@ -7,14 +7,12 @@ import { Param, UseGuards, Request, - Query, } from '@nestjs/common'; import { LoggerService } from '@@core/logger/logger.service'; import { ApiBody, ApiResponse, ApiTags, ApiOperation } from '@nestjs/swagger'; import { WebhookService } from './webhook.service'; import { WebhookDto } from './dto/webhook.dto'; import { JwtAuthGuard } from '@@core/auth/guards/jwt-auth.guard'; -import { ValidateUserGuard } from '@@core/utils/guards/validate-user.guard'; @ApiTags('webhook') @Controller('webhook') @@ -31,10 +29,11 @@ export class WebhookController { summary: 'Retrieve webhooks metadata ', }) @ApiResponse({ status: 200 }) - @UseGuards(JwtAuthGuard, ValidateUserGuard) + @UseGuards(JwtAuthGuard) @Get() - getWebhooks(@Query('project_id') project_id: string) { - return this.webhookService.getWebhookEndpoints(project_id); + getWebhooks(@Request() req: any) { + const { id_project } = req.user; + return this.webhookService.getWebhookEndpoints(id_project); } @ApiOperation({ @@ -46,9 +45,7 @@ export class WebhookController { async updateWebhookStatus( @Param('id') id: string, @Body('active') active: boolean, - @Request() req: any, ) { - // verify id of webhook belongs to user from req return this.webhookService.updateStatusWebhookEndpoint(id, active); } @@ -61,7 +58,6 @@ export class WebhookController { @UseGuards(JwtAuthGuard) @Post() async addWebhook(@Body() data: WebhookDto) { - // verify project id of user is same from data return this.webhookService.createWebhookEndpoint(data); } } diff --git a/packages/api/src/crm/@utils/@types/index.ts b/packages/api/src/crm/@utils/@types/index.ts index ecb0bd329..8e85b11a6 100644 --- a/packages/api/src/crm/@utils/@types/index.ts +++ b/packages/api/src/crm/@utils/@types/index.ts @@ -48,6 +48,7 @@ import { UnifiedUserInput, UnifiedUserOutput, } from '@crm/user/types/model.unified'; +import { IsIn, IsOptional, IsString } from 'class-validator'; export enum CrmObject { company = 'company', @@ -103,143 +104,336 @@ export type ICrmService = | IStageService | ICompanyService; -/* contact */ -/* -export * from '../../contact/services/freshsales/types'; -export * from '../../contact/services/zendesk/types'; -export * from '../../contact/services/hubspot/types'; -export * from '../../contact/services/zoho/types'; -export * from '../../contact/services/pipedrive/types'; -export * from '../../contact/services/attio/types' -*/ -/* user */ -/* -export * from '../../user/services/freshsales/types'; -export * from '../../user/services/zendesk/types'; -export * from '../../user/services/hubspot/types'; -export * from '../../user/services/zoho/types'; -export * from '../../user/services/pipedrive/types'; -*/ -/* engagement */ -/* -export * from '../../engagement/services/freshsales/types'; -export * from '../../engagement/services/zendesk/types'; -export * from '../../engagement/services/hubspot/types'; -export * from '../../engagement/services/zoho/types'; -export * from '../../engagement/services/pipedrive/types'; -*/ -/* note */ -/* -export * from '../../note/services/freshsales/types'; -export * from '../../note/services/zendesk/types'; -export * from '../../note/services/hubspot/types'; -export * from '../../note/services/zoho/types'; -export * from '../../note/services/pipedrive/types'; -*/ -/* deal */ -/* -export * from '../../deal/services/freshsales/types'; -export * from '../../deal/services/zendesk/types'; -export * from '../../deal/services/hubspot/types'; -export * from '../../deal/services/zoho/types'; -export * from '../../deal/services/pipedrive/types'; -*/ -/* task */ -/* -export * from '../../task/services/freshsales/types'; -export * from '../../task/services/zendesk/types'; -export * from '../../task/services/hubspot/types'; -export * from '../../task/services/zoho/types'; -export * from '../../task/services/pipedrive/types'; -*/ -/* stage */ -/* -export * from '../../stage/services/freshsales/types'; -export * from '../../stage/services/zendesk/types'; -export * from '../../stage/services/hubspot/types'; -export * from '../../stage/services/zoho/types'; -export * from '../../stage/services/pipedrive/types'; -*/ -/* company */ -/*export * from '../../company/services/freshsales/types'; -export * from '../../company/services/zendesk/types'; -export * from '../../company/services/hubspot/types'; -export * from '../../company/services/zoho/types'; -export * from '../../company/services/pipedrive/types'; -*/ - -/* engagementType */ +export enum Industry { + ACCOUNTING = 'ACCOUNTING', + AIRLINES_AVIATION = 'AIRLINES_AVIATION', + ALTERNATIVE_DISPUTE_RESOLUTION = 'ALTERNATIVE_DISPUTE_RESOLUTION', + ALTERNATIVE_MEDICINE = 'ALTERNATIVE_MEDICINE', + ANIMATION = 'ANIMATION', + APPAREL_FASHION = 'APPAREL_FASHION', + ARCHITECTURE_PLANNING = 'ARCHITECTURE_PLANNING', + ARTS_AND_CRAFTS = 'ARTS_AND_CRAFTS', + AUTOMOTIVE = 'AUTOMOTIVE', + AVIATION_AEROSPACE = 'AVIATION_AEROSPACE', + BANKING = 'BANKING', + BIOTECHNOLOGY = 'BIOTECHNOLOGY', + BROADCAST_MEDIA = 'BROADCAST_MEDIA', + BUILDING_MATERIALS = 'BUILDING_MATERIALS', + BUSINESS_SUPPLIES_AND_EQUIPMENT = 'BUSINESS_SUPPLIES_AND_EQUIPMENT', + CAPITAL_MARKETS = 'CAPITAL_MARKETS', + CHEMICALS = 'CHEMICALS', + CIVIC_SOCIAL_ORGANIZATION = 'CIVIC_SOCIAL_ORGANIZATION', + CIVIL_ENGINEERING = 'CIVIL_ENGINEERING', + COMMERCIAL_REAL_ESTATE = 'COMMERCIAL_REAL_ESTATE', + COMPUTER_NETWORK_SECURITY = 'COMPUTER_NETWORK_SECURITY', + COMPUTER_GAMES = 'COMPUTER_GAMES', + COMPUTER_HARDWARE = 'COMPUTER_HARDWARE', + COMPUTER_NETWORKING = 'COMPUTER_NETWORKING', + COMPUTER_SOFTWARE = 'COMPUTER_SOFTWARE', + INTERNET = 'INTERNET', + CONSTRUCTION = 'CONSTRUCTION', + CONSUMER_ELECTRONICS = 'CONSUMER_ELECTRONICS', + CONSUMER_GOODS = 'CONSUMER_GOODS', + CONSUMER_SERVICES = 'CONSUMER_SERVICES', + COSMETICS = 'COSMETICS', + DAIRY = 'DAIRY', + DEFENSE_SPACE = 'DEFENSE_SPACE', + DESIGN = 'DESIGN', + EDUCATION_MANAGEMENT = 'EDUCATION_MANAGEMENT', + E_LEARNING = 'E_LEARNING', + ELECTRICAL_ELECTRONIC_MANUFACTURING = 'ELECTRICAL_ELECTRONIC_MANUFACTURING', + ENTERTAINMENT = 'ENTERTAINMENT', + ENVIRONMENTAL_SERVICES = 'ENVIRONMENTAL_SERVICES', + EVENTS_SERVICES = 'EVENTS_SERVICES', + EXECUTIVE_OFFICE = 'EXECUTIVE_OFFICE', + FACILITIES_SERVICES = 'FACILITIES_SERVICES', + FARMING = 'FARMING', + FINANCIAL_SERVICES = 'FINANCIAL_SERVICES', + FINE_ART = 'FINE_ART', + FISHERY = 'FISHERY', + FOOD_BEVERAGES = 'FOOD_BEVERAGES', + FOOD_PRODUCTION = 'FOOD_PRODUCTION', + FUND_RAISING = 'FUND_RAISING', + FURNITURE = 'FURNITURE', + GAMBLING_CASINOS = 'GAMBLING_CASINOS', + GLASS_CERAMICS_CONCRETE = 'GLASS_CERAMICS_CONCRETE', + GOVERNMENT_ADMINISTRATION = 'GOVERNMENT_ADMINISTRATION', + GOVERNMENT_RELATIONS = 'GOVERNMENT_RELATIONS', + GRAPHIC_DESIGN = 'GRAPHIC_DESIGN', + HEALTH_WELLNESS_AND_FITNESS = 'HEALTH_WELLNESS_AND_FITNESS', + HIGHER_EDUCATION = 'HIGHER_EDUCATION', + HOSPITAL_HEALTH_CARE = 'HOSPITAL_HEALTH_CARE', + HOSPITALITY = 'HOSPITALITY', + HUMAN_RESOURCES = 'HUMAN_RESOURCES', + IMPORT_AND_EXPORT = 'IMPORT_AND_EXPORT', + INDIVIDUAL_FAMILY_SERVICES = 'INDIVIDUAL_FAMILY_SERVICES', + INDUSTRIAL_AUTOMATION = 'INDUSTRIAL_AUTOMATION', + INFORMATION_SERVICES = 'INFORMATION_SERVICES', + INFORMATION_TECHNOLOGY_AND_SERVICES = 'INFORMATION_TECHNOLOGY_AND_SERVICES', + INSURANCE = 'INSURANCE', + INTERNATIONAL_AFFAIRS = 'INTERNATIONAL_AFFAIRS', + INTERNATIONAL_TRADE_AND_DEVELOPMENT = 'INTERNATIONAL_TRADE_AND_DEVELOPMENT', + INVESTMENT_BANKING = 'INVESTMENT_BANKING', + INVESTMENT_MANAGEMENT = 'INVESTMENT_MANAGEMENT', + JUDICIARY = 'JUDICIARY', + LAW_ENFORCEMENT = 'LAW_ENFORCEMENT', + LAW_PRACTICE = 'LAW_PRACTICE', + LEGAL_SERVICES = 'LEGAL_SERVICES', + LEGISLATIVE_OFFICE = 'LEGISLATIVE_OFFICE', + LEISURE_TRAVEL_TOURISM = 'LEISURE_TRAVEL_TOURISM', + LIBRARIES = 'LIBRARIES', + LOGISTICS_AND_SUPPLY_CHAIN = 'LOGISTICS_AND_SUPPLY_CHAIN', + LUXURY_GOODS_JEWELRY = 'LUXURY_GOODS_JEWELRY', + MACHINERY = 'MACHINERY', + MANAGEMENT_CONSULTING = 'MANAGEMENT_CONSULTING', + MARITIME = 'MARITIME', + MARKET_RESEARCH = 'MARKET_RESEARCH', + MARKETING_AND_ADVERTISING = 'MARKETING_AND_ADVERTISING', + MECHANICAL_OR_INDUSTRIAL_ENGINEERING = 'MECHANICAL_OR_INDUSTRIAL_ENGINEERING', + MEDIA_PRODUCTION = 'MEDIA_PRODUCTION', + MEDICAL_DEVICES = 'MEDICAL_DEVICES', + MEDICAL_PRACTICE = 'MEDICAL_PRACTICE', + MENTAL_HEALTH_CARE = 'MENTAL_HEALTH_CARE', + MILITARY = 'MILITARY', + MINING_METALS = 'MINING_METALS', + MOTION_PICTURES_AND_FILM = 'MOTION_PICTURES_AND_FILM', + MUSEUMS_AND_INSTITUTIONS = 'MUSEUMS_AND_INSTITUTIONS', + MUSIC = 'MUSIC', + NANOTECHNOLOGY = 'NANOTECHNOLOGY', + NEWSPAPERS = 'NEWSPAPERS', + NON_PROFIT_ORGANIZATION_MANAGEMENT = 'NON_PROFIT_ORGANIZATION_MANAGEMENT', + OIL_ENERGY = 'OIL_ENERGY', + ONLINE_MEDIA = 'ONLINE_MEDIA', + OUTSOURCING_OFFSHORING = 'OUTSOURCING_OFFSHORING', + PACKAGE_FREIGHT_DELIVERY = 'PACKAGE_FREIGHT_DELIVERY', + PACKAGING_AND_CONTAINERS = 'PACKAGING_AND_CONTAINERS', + PAPER_FOREST_PRODUCTS = 'PAPER_FOREST_PRODUCTS', + PERFORMING_ARTS = 'PERFORMING_ARTS', + PHARMACEUTICALS = 'PHARMACEUTICALS', + PHILANTHROPY = 'PHILANTHROPY', + PHOTOGRAPHY = 'PHOTOGRAPHY', + PLASTICS = 'PLASTICS', + POLITICAL_ORGANIZATION = 'POLITICAL_ORGANIZATION', + PRIMARY_SECONDARY_EDUCATION = 'PRIMARY_SECONDARY_EDUCATION', + PRINTING = 'PRINTING', + PROFESSIONAL_TRAINING_COACHING = 'PROFESSIONAL_TRAINING_COACHING', + PROGRAM_DEVELOPMENT = 'PROGRAM_DEVELOPMENT', + PUBLIC_POLICY = 'PUBLIC_POLICY', + PUBLIC_RELATIONS_AND_COMMUNICATIONS = 'PUBLIC_RELATIONS_AND_COMMUNICATIONS', + PUBLIC_SAFETY = 'PUBLIC_SAFETY', + PUBLISHING = 'PUBLISHING', + RAILROAD_MANUFACTURE = 'RAILROAD_MANUFACTURE', + RANCHING = 'RANCHING', + REAL_ESTATE = 'REAL_ESTATE', + RECREATIONAL_FACILITIES_AND_SERVICES = 'RECREATIONAL_FACILITIES_AND_SERVICES', + RELIGIOUS_INSTITUTIONS = 'RELIGIOUS_INSTITUTIONS', + RENEWABLES_ENVIRONMENT = 'RENEWABLES_ENVIRONMENT', + RESEARCH = 'RESEARCH', + RESTAURANTS = 'RESTAURANTS', + RETAIL = 'RETAIL', + SECURITY_AND_INVESTIGATIONS = 'SECURITY_AND_INVESTIGATIONS', + SEMICONDUCTORS = 'SEMICONDUCTORS', + SHIPBUILDING = 'SHIPBUILDING', + SPORTING_GOODS = 'SPORTING_GOODS', + SPORTS = 'SPORTS', + STAFFING_AND_RECRUITING = 'STAFFING_AND_RECRUITING', + SUPERMARKETS = 'SUPERMARKETS', + TELECOMMUNICATIONS = 'TELECOMMUNICATIONS', + TEXTILES = 'TEXTILES', + THINK_TANKS = 'THINK_TANKS', + TOBACCO = 'TOBACCO', + TRANSLATION_AND_LOCALIZATION = 'TRANSLATION_AND_LOCALIZATION', + TRANSPORTATION_TRUCKING_RAILROAD = 'TRANSPORTATION_TRUCKING_RAILROAD', + UTILITIES = 'UTILITIES', + VENTURE_CAPITAL_PRIVATE_EQUITY = 'VENTURE_CAPITAL_PRIVATE_EQUITY', + VETERINARY = 'VETERINARY', + WAREHOUSING = 'WAREHOUSING', + WHOLESALE = 'WHOLESALE', + WINE_AND_SPIRITS = 'WINE_AND_SPIRITS', + WIRELESS = 'WIRELESS', + WRITING_AND_EDITING = 'WRITING_AND_EDITING', +} + +export const countryPhoneFormats: { [countryCode: string]: string } = { + '+1': 'NNN-NNN-NNNN', // USA + '+44': 'NNNN NNNNNN', // UK + '+49': 'NNN NNNNNNN', // Germany + '+33': 'N NN NN NN NN', // France + '+81': 'NNN NNNN NNNN', // Japan + '+91': 'NNNNN NNNNNN', // India + '+86': 'NNN NNNN NNNN', // China + '+7': 'NNN NNN-NN-NN', // Russia + '+55': 'NN NNNNN-NNNN', // Brazil + '+61': 'N NNNN NNNN', // Australia + '+39': 'NNN NNNN NNNN', // Italy + '+34': 'N NNN NNNN', // Spain + '+62': 'NNN NNN-NNNN', // Indonesia + '+27': 'NNN NNN NNNN', // South Africa + '+82': 'NNN-NNNN-NNNN', // South Korea + '+52': 'NN NNNN NNNN', // Mexico + '+31': 'NN NNN NNNN', // Netherlands + '+90': 'NNN NNN NN NN', // Turkey + '+966': 'N NNN NNNN', // Saudi Arabia + '+48': 'NN NNN NN NN', // Poland + '+47': 'NNN NN NNN', // Norway + '+46': 'NNN-NNN NN NN', // Sweden + '+41': 'NNN NNN NN NN', // Switzerland + '+60': 'NN NNN NNNN', // Malaysia + '+66': 'N NNN NNNN', // Thailand + '+63': 'NNN NNN NNNN', // Philippines + '+64': 'NN NNN NNNN', // New Zealand + '+358': 'NNN NNNNNNN', // Finland + '+32': 'NNN NN NN NN', // Belgium + '+43': 'NNN NNNNNNN', // Austria + '+20': 'NNN NNNN NNNN', // Egypt + '+98': 'NNN NNN NNNN', // Iran + '+54': 'NN NNNN-NNNN', // Argentina + '+84': 'NNN NNNN NNNN', // Vietnam + '+380': 'NN NNN NNNN', // Ukraine + '+234': 'NNN NNN NNNN', // Nigeria + '+92': 'NNN NNNNNNN', // Pakistan + '+880': 'NNNN NNNNNN', // Bangladesh + '+30': 'NNN NNN NNNN', // Greece + '+351': 'NN NNN NNNN', // Portugal + '+36': 'NNN NNN NNN', // Hungary + '+40': 'NNN NNN NNN', // Romania + '+56': 'N NNNN NNNN', // Chile + '+94': 'NN NNN NNNN', // Sri Lanka + '+65': 'NNNN NNNN', // Singapore + '+375': 'NNN NN-NN-NN', // Belarus + '+353': 'NN NNN NNNN', // Ireland + '+45': 'NN NN NN NN', // Denmark + '+421': 'NNN NNN NNN', // Slovakia + '+386': 'NNN NNN NNN', // Slovenia + '+971': 'NN NNN NNNN', // UAE + '+972': 'NNN NNN NNNN', // Israel + '+852': 'NNNN NNNN', // Hong Kong + '+385': 'NNN NNNN', // Croatia + '+387': 'NNN NNNN', // Bosnia and Herzegovina + '+389': 'NN NNN NNN', // North Macedonia + '+381': 'NNN NNNN', // Serbia + '+373': 'NNN NNNN', // Moldova + '+995': 'NNN NNN NNN', // Georgia + '+374': 'NN NNNNNN', // Armenia + '+993': 'NNN NNNNN', // Turkmenistan + '+996': 'NNN NNNNNN', // Kyrgyzstan + '+998': 'NN NNN NNNN', // Uzbekistan + '+976': 'NN NNN NNNN', // Mongolia + '+855': 'NNN NNN NNN', // Cambodia + '+856': 'NNN NNN NNNN', // Laos +}; export class Email { @ApiProperty({ + type: String, description: 'The email address', }) + @IsString() email_address: string; @ApiProperty({ - description: 'The email address type', + type: String, + description: + 'The email address type. Authorized values are either PERSONAL or WORK.', }) + @IsIn(['PERSONAL', 'WORK']) + @IsString() email_address_type: string; @ApiPropertyOptional({ + type: String, description: 'The owner type of an email', }) + @IsString() + @IsOptional() owner_type?: string; } export class Phone { @ApiProperty({ - description: 'The phone number', + type: String, + description: + 'The phone number starting with a plus (+) followed by the country code (e.g +336676778890 for France)', }) + @IsString() phone_number: string; @ApiProperty({ - description: 'The phone type', + type: String, + description: 'The phone type. Authorized values are either MOBILE or WORK', }) + @IsIn(['MOBILE', 'WORK']) + @IsString() phone_type: string; - @ApiPropertyOptional({ description: 'The owner type of a phone number' }) + @ApiPropertyOptional({ + type: String, + description: 'The owner type of a phone number', + }) + @IsString() + @IsOptional() owner_type?: string; } export class Address { @ApiProperty({ + type: String, description: 'The street', }) + @IsString() street_1: string; @ApiProperty({ + type: String, description: 'More information about the street ', }) + @IsString() + @IsOptional() street_2?: string; @ApiProperty({ + type: String, description: 'The city', }) + @IsString() city: string; @ApiProperty({ + type: String, description: 'The state', }) + @IsString() state: string; @ApiProperty({ + type: String, description: 'The postal code', }) + @IsString() postal_code: string; @ApiProperty({ - description: 'The country', + type: String, + description: 'The country.', }) + @IsString() country: string; @ApiProperty({ - description: 'The address type', + type: String, + description: + 'The address type. Authorized values are either PERSONAL or WORK.', }) + @IsIn(['PERSONAL', 'WORK']) + @IsOptional() + @IsString() address_type?: string; @ApiProperty({ + type: String, description: 'The owner type of the address', }) + @IsOptional() + @IsString() owner_type?: string; } diff --git a/packages/api/src/crm/company/company.module.ts b/packages/api/src/crm/company/company.module.ts index 990390470..3c6450280 100644 --- a/packages/api/src/crm/company/company.module.ts +++ b/packages/api/src/crm/company/company.module.ts @@ -17,9 +17,10 @@ import { AttioService } from './services/attio'; @Module({ imports: [ - BullModule.registerQueue({ - name: 'webhookDelivery', - }), + BullModule.registerQueue( + { name: 'webhookDelivery' }, + { name: 'syncTasks' }, + ), ], controllers: [CompanyController], providers: [ @@ -38,6 +39,13 @@ import { AttioService } from './services/attio'; HubspotService, AttioService, ], - exports: [SyncService], + exports: [ + SyncService, + ServiceRegistry, + WebhookService, + FieldMappingService, + LoggerService, + PrismaService, + ], }) export class CompanyModule {} diff --git a/packages/api/src/crm/company/services/attio/mappers.ts b/packages/api/src/crm/company/services/attio/mappers.ts index b406f28d0..96912c3db 100644 --- a/packages/api/src/crm/company/services/attio/mappers.ts +++ b/packages/api/src/crm/company/services/attio/mappers.ts @@ -35,20 +35,7 @@ export class AttioCompanyMapper implements ICompanyMapper { }, ]; } - // const result: AttioCompanyInput = { - // city: '', - // name: source.name, - // phone: '', - // state: '', - // domain: '', - // industry: source.industry, - // }; - // Assuming 'phone_numbers' array contains at least one phone number - // const primaryPhone = source.phone_numbers?.[0]?.phone_number; - // if (primaryPhone) { - // result.values = primaryPhone; - // } if (source.addresses) { const address = source.addresses[0]; if (address) { diff --git a/packages/api/src/crm/company/sync/sync.processor.ts b/packages/api/src/crm/company/sync/sync.processor.ts new file mode 100644 index 000000000..42823a445 --- /dev/null +++ b/packages/api/src/crm/company/sync/sync.processor.ts @@ -0,0 +1,17 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; + +@Processor('syncTasks') +export class SyncProcessor { + constructor(private syncService: SyncService) {} + @Process('crm-sync-companies') + async handleSyncCompanies(job: Job) { + try { + console.log(`Processing queue -> crm-sync-companies ${job.id}`); + await this.syncService.syncCompanies(); + } catch (error) { + console.error('Error syncing crm companies', error); + } + } +} diff --git a/packages/api/src/crm/company/sync/sync.service.ts b/packages/api/src/crm/company/sync/sync.service.ts index c3aadca02..d712bac5f 100644 --- a/packages/api/src/crm/company/sync/sync.service.ts +++ b/packages/api/src/crm/company/sync/sync.service.ts @@ -17,6 +17,8 @@ import { crm_companies as CrmCompany } from '@prisma/client'; import { normalizeAddresses } from '../utils'; import { Utils } from '@crm/contact/utils'; import { CRM_PROVIDERS } from '@panora/shared'; +import { Queue } from 'bull'; +import { InjectQueue } from '@nestjs/bull'; @Injectable() export class SyncService implements OnModuleInit { @@ -28,6 +30,7 @@ export class SyncService implements OnModuleInit { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + @InjectQueue('syncTasks') private syncQueue: Queue, ) { this.logger.setContext(SyncService.name); this.utils = new Utils(); @@ -35,19 +38,59 @@ export class SyncService implements OnModuleInit { async onModuleInit() { try { - await this.syncCompanies(); + await this.scheduleSyncJob(); } catch (error) { handleServiceError(error, this.logger); } } - @Cron('*/20 * * * *') + private async scheduleSyncJob() { + const jobName = 'crm-sync-companies'; + + // Remove existing jobs to avoid duplicates in case of application restart + const jobs = await this.syncQueue.getRepeatableJobs(); + for (const job of jobs) { + if (job.name === jobName) { + await this.syncQueue.removeRepeatableByKey(job.key); + } + } + // Add new job to the queue with a CRON expression + await this.syncQueue.add( + jobName, + {}, + { + repeat: { cron: '0 0 * * *' }, // Runs once a day at midnight + }, + ); + } + //function used by sync worker which populate our crm_companies table //its role is to fetch all companies from providers 3rd parties and save the info inside our db - async syncCompanies() { + //@Cron('*/2 * * * *') // every 2 minutes (for testing) + @Cron('0 */8 * * *') // every 8 hours + async syncCompanies(user_id?: string) { try { this.logger.log(`Syncing companies....`); - const users = await this.prisma.users.findMany(); + // TODO: insert inside sync_jobs table ? + /* + { + "common_object": "company", + "vertical": "crm", + "last_sync_start": "", + "next_sync_start": "", + "status": "SYNCING", + "is_initial_sync": true, + } + */ + const users = user_id + ? [ + await this.prisma.users.findUnique({ + where: { + id_user: user_id, + }, + }), + ] + : await this.prisma.users.findMany(); if (users && users.length > 0) { for (const user of users) { const projects = await this.prisma.projects.findMany({ diff --git a/packages/api/src/crm/company/types/model.unified.ts b/packages/api/src/crm/company/types/model.unified.ts index 89203d42d..f0635f997 100644 --- a/packages/api/src/crm/company/types/model.unified.ts +++ b/packages/api/src/crm/company/types/model.unified.ts @@ -1,39 +1,62 @@ -import { Address, Email, Phone } from '@crm/@utils/@types'; +import { Address, Email, Industry, Phone } from '@crm/@utils/@types'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsEnum, + IsNumber, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; export class UnifiedCompanyInput { - @ApiProperty({ description: 'The name of the company' }) + @ApiProperty({ type: String, description: 'The name of the company' }) + @IsString() name: string; - @ApiPropertyOptional({ description: 'The industry of the company' }) + @ApiPropertyOptional({ + type: String, + description: + 'The industry of the company. Authorized values can be found in the Industry enum.', + }) + @IsEnum(Industry) + @IsOptional() industry?: string; @ApiPropertyOptional({ + type: Number, description: 'The number of employees of the company', }) + @IsNumber() + @IsOptional() number_of_employees?: number; @ApiPropertyOptional({ + type: String, description: 'The uuid of the user who owns the company', }) + @IsOptional() + @IsUUID() user_id?: string; @ApiPropertyOptional({ description: 'The email addresses of the company', type: [Email], }) + @IsOptional() email_addresses?: Email[]; @ApiPropertyOptional({ description: 'The addresses of the company', type: [Address], }) + @IsOptional() addresses?: Address[]; @ApiPropertyOptional({ description: 'The phone numbers of the company', type: [Phone], }) + @IsOptional() phone_numbers?: Phone[]; @ApiPropertyOptional({ @@ -41,22 +64,29 @@ export class UnifiedCompanyInput { description: 'The custom field mappings of the company between the remote 3rd party & Panora', }) + @IsOptional() field_mappings?: Record; } export class UnifiedCompanyOutput extends UnifiedCompanyInput { - @ApiPropertyOptional({ description: 'The uuid of the company' }) + @ApiPropertyOptional({ type: String, description: 'The uuid of the company' }) + @IsUUID() + @IsOptional() id?: string; @ApiPropertyOptional({ + type: String, description: 'The id of the company in the context of the Crm 3rd Party', }) + @IsString() + @IsOptional() remote_id?: string; @ApiPropertyOptional({ - type: [{}], + type: {}, description: 'The remote data of the company in the context of the Crm 3rd Party', }) + @IsOptional() remote_data?: Record; } diff --git a/packages/api/src/crm/contact/contact.module.ts b/packages/api/src/crm/contact/contact.module.ts index a8fae3a18..5a3de9885 100644 --- a/packages/api/src/crm/contact/contact.module.ts +++ b/packages/api/src/crm/contact/contact.module.ts @@ -9,7 +9,7 @@ import { PipedriveService } from './services/pipedrive'; import { HubspotService } from './services/hubspot'; import { LoggerService } from '@@core/logger/logger.service'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; -import { SyncContactsService } from './sync/sync.service'; +import { SyncService } from './sync/sync.service'; import { WebhookService } from '@@core/webhook/webhook.service'; import { BullModule } from '@nestjs/bull'; import { EncryptionService } from '@@core/encryption/encryption.service'; @@ -17,9 +17,12 @@ import { ServiceRegistry } from './services/registry.service'; @Module({ imports: [ - BullModule.registerQueue({ - name: 'webhookDelivery', - }), + BullModule.registerQueue( + { + name: 'webhookDelivery', + }, + { name: 'syncTasks' }, + ), ], controllers: [ContactController], providers: [ @@ -27,7 +30,7 @@ import { ServiceRegistry } from './services/registry.service'; PrismaService, LoggerService, FieldMappingService, - SyncContactsService, + SyncService, WebhookService, EncryptionService, ServiceRegistry, @@ -38,6 +41,13 @@ import { ServiceRegistry } from './services/registry.service'; PipedriveService, HubspotService, ], - exports: [SyncContactsService], + exports: [ + SyncService, + ServiceRegistry, + WebhookService, + FieldMappingService, + LoggerService, + PrismaService, + ], }) export class ContactModule {} diff --git a/packages/api/src/crm/contact/services/attio/mappers.ts b/packages/api/src/crm/contact/services/attio/mappers.ts index a78ae25e6..63398e15a 100644 --- a/packages/api/src/crm/contact/services/attio/mappers.ts +++ b/packages/api/src/crm/contact/services/attio/mappers.ts @@ -42,24 +42,9 @@ export class AttioContactMapper implements IContactMapper { } if (primaryPhone) { - result.values.phone_numbers = [{ original_phone_number: primaryPhone }]; + result.values.phone_numbers = [primaryPhone]; } - // if (source.user_id) { - // const owner = await this.utils.getUser(source.user_id); - // if (owner) { - // result.id = { - // object_id: Number(owner.remote_id), - // name: owner.name, - // email: owner.email, - // has_pic: 0, - // pic_hash: '', - // active_flag: false, - // value: 0, - // }; - // } - // } - if (customFieldMappings && source.field_mappings) { for (const [k, v] of Object.entries(source.field_mappings)) { const mapping = customFieldMappings.find( diff --git a/packages/api/src/crm/contact/services/attio/types.ts b/packages/api/src/crm/contact/services/attio/types.ts index 950da6ac9..c4bb4d423 100644 --- a/packages/api/src/crm/contact/services/attio/types.ts +++ b/packages/api/src/crm/contact/services/attio/types.ts @@ -120,7 +120,7 @@ export interface AttioContact { twitter_follower_count?: NumberValueItem[]; instagram?: TextValueItem[]; first_email_interaction?: InteractionValueItem[]; - phone_numbers?: PhoneValueItem[]; + phone_numbers?: PhoneValueItem[] | string[]; }; } diff --git a/packages/api/src/crm/contact/sync/sync.processor.ts b/packages/api/src/crm/contact/sync/sync.processor.ts new file mode 100644 index 000000000..aafd01b46 --- /dev/null +++ b/packages/api/src/crm/contact/sync/sync.processor.ts @@ -0,0 +1,18 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; + +@Processor('syncTasks') +export class SyncProcessor { + constructor(private syncService: SyncService) {} + + @Process({ name: 'crm-sync-contacts', concurrency: 5 }) + async handleSyncContacts(job: Job) { + try { + console.log(`Processing queue -> crm-sync-contacts ${job.id}`); + await this.syncService.syncContacts(); + } catch (error) { + console.error('Error syncing crm contacts', error); + } + } +} diff --git a/packages/api/src/crm/contact/sync/sync.service.ts b/packages/api/src/crm/contact/sync/sync.service.ts index f37a6f303..889b0015d 100644 --- a/packages/api/src/crm/contact/sync/sync.service.ts +++ b/packages/api/src/crm/contact/sync/sync.service.ts @@ -17,9 +17,11 @@ import { ServiceRegistry } from '../services/registry.service'; import { normalizeAddresses } from '@crm/company/utils'; import { Utils } from '../utils'; import { CRM_PROVIDERS } from '@panora/shared'; +import { InjectQueue } from '@nestjs/bull'; +import { Queue } from 'bull'; @Injectable() -export class SyncContactsService implements OnModuleInit { +export class SyncService implements OnModuleInit { private readonly utils: Utils; constructor( @@ -28,26 +30,69 @@ export class SyncContactsService implements OnModuleInit { private fieldMappingService: FieldMappingService, private webhook: WebhookService, private serviceRegistry: ServiceRegistry, + @InjectQueue('syncTasks') private syncQueue: Queue, ) { - this.logger.setContext(SyncContactsService.name); + this.logger.setContext(SyncService.name); this.utils = new Utils(); } async onModuleInit() { try { - await this.syncContacts(); + await this.scheduleSyncJob(); } catch (error) { handleServiceError(error, this.logger); } } - @Cron('*/20 * * * *') + private async scheduleSyncJob() { + const jobName = 'crm-sync-contacts'; + + // Remove existing jobs to avoid duplicates in case of application restart + const jobs = await this.syncQueue.getRepeatableJobs(); + console.log(`Found ${jobs.length} repeatable jobs.`); + for (const job of jobs) { + console.log(`Checking job: ${job.name}`); + if (job.name === jobName) { + console.log(`Removing job with key: ${job.key}`); + await this.syncQueue.removeRepeatableByKey(job.key); + } + } + + // Add new job to the queue with a CRON expression + console.log(`Adding new job: ${jobName}`); + await this.syncQueue + .add( + jobName, + {}, + { + repeat: { cron: '*/2 * * * *' }, // Runs once a day at midnight + }, + ) + .then(() => { + console.log('Job added successfully'); + }) + .catch((error) => { + console.error('Failed to add job', error); + }); + } + //function used by sync worker which populate our crm_contacts table //its role is to fetch all contacts from providers 3rd parties and save the info inside our db - async syncContacts() { + //@Cron('*/2 * * * *') // every 2 minutes (for testing) + @Cron('0 */8 * * *') // every 8 hours + async syncContacts(user_id?: string) { try { this.logger.log(`Syncing contacts....`); - const users = await this.prisma.users.findMany(); + + const users = user_id + ? [ + await this.prisma.users.findUnique({ + where: { + id_user: user_id, + }, + }), + ] + : await this.prisma.users.findMany(); if (users && users.length > 0) { for (const user of users) { const projects = await this.prisma.projects.findMany({ diff --git a/packages/api/src/crm/contact/types/model.unified.ts b/packages/api/src/crm/contact/types/model.unified.ts index 83182e2bf..1a1266b1c 100644 --- a/packages/api/src/crm/contact/types/model.unified.ts +++ b/packages/api/src/crm/contact/types/model.unified.ts @@ -1,35 +1,43 @@ import { Address, Email, Phone } from '@crm/@utils/@types'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsOptional, IsString, IsUUID } from 'class-validator'; export class UnifiedContactInput { - @ApiProperty({ description: 'The first name of the contact' }) + @ApiProperty({ type: String, description: 'The first name of the contact' }) + @IsString() first_name: string; - @ApiProperty({ description: 'The last name of the contact' }) + @ApiProperty({ type: String, description: 'The last name of the contact' }) + @IsString() last_name: string; @ApiPropertyOptional({ type: [Email], description: 'The email addresses of the contact', }) - email_addresses: Email[]; + @IsOptional() + email_addresses?: Email[]; @ApiPropertyOptional({ type: [Phone], description: 'The phone numbers of the contact', }) - phone_numbers: Phone[]; + @IsOptional() + phone_numbers?: Phone[]; @ApiPropertyOptional({ type: [Address], description: 'The addresses of the contact', }) - addresses: Address[]; + @IsOptional() + addresses?: Address[]; @ApiPropertyOptional({ type: String, description: 'The uuid of the user who owns the contact', }) + @IsUUID() + @IsOptional() user_id?: string; @ApiPropertyOptional({ @@ -37,22 +45,29 @@ export class UnifiedContactInput { description: 'The custom field mappings of the contact between the remote 3rd party & Panora', }) + @IsOptional() field_mappings?: Record; } export class UnifiedContactOutput extends UnifiedContactInput { - @ApiPropertyOptional({ description: 'The uuid of the contact' }) + @ApiPropertyOptional({ type: String, description: 'The uuid of the contact' }) + @IsUUID() + @IsOptional() id?: string; @ApiPropertyOptional({ + type: String, description: 'The id of the contact in the context of the Crm 3rd Party', }) + @IsString() + @IsOptional() remote_id?: string; @ApiPropertyOptional({ - type: [{}], + type: {}, description: 'The remote data of the contact in the context of the Crm 3rd Party', }) + @IsOptional() remote_data?: Record; } diff --git a/packages/api/src/crm/contact/utils/index.ts b/packages/api/src/crm/contact/utils/index.ts index e7ced2ea9..4af036fa9 100644 --- a/packages/api/src/crm/contact/utils/index.ts +++ b/packages/api/src/crm/contact/utils/index.ts @@ -1,4 +1,4 @@ -import { Email, Phone } from '@crm/@utils/@types'; +import { countryPhoneFormats, Email, Phone } from '@crm/@utils/@types'; import { v4 as uuidv4 } from 'uuid'; import { PrismaClient } from '@prisma/client'; @@ -44,6 +44,26 @@ export class Utils { normalizedPhones, }; } + + extractPhoneDetails(phone_number: string): { + country_code: string; + base_number: string; + } { + let country_code = ''; + let base_number: string = phone_number; + + // Find the matching country code + for (const [countryCode, _] of Object.entries(countryPhoneFormats)) { + if (phone_number.startsWith(countryCode)) { + country_code = countryCode; + base_number = phone_number.substring(countryCode.length); + break; + } + } + + return { country_code, base_number }; + } + async getRemoteIdFromUserUuid(uuid: string) { try { const res = await this.prisma.crm_users.findFirst({ diff --git a/packages/api/src/crm/crm.module.ts b/packages/api/src/crm/crm.module.ts index c2d058b21..3edf3c72a 100644 --- a/packages/api/src/crm/crm.module.ts +++ b/packages/api/src/crm/crm.module.ts @@ -20,7 +20,6 @@ import { CompanyModule } from './company/company.module'; UserModule, ], providers: [], - controllers: [], exports: [ ContactModule, DealModule, diff --git a/packages/api/src/crm/deal/deal.module.ts b/packages/api/src/crm/deal/deal.module.ts index 5c3dbfd97..ba2f66537 100644 --- a/packages/api/src/crm/deal/deal.module.ts +++ b/packages/api/src/crm/deal/deal.module.ts @@ -16,9 +16,12 @@ import { ZohoService } from './services/zoho'; @Module({ imports: [ - BullModule.registerQueue({ - name: 'webhookDelivery', - }), + BullModule.registerQueue( + { + name: 'webhookDelivery', + }, + { name: 'syncTasks' }, + ), ], controllers: [DealController], providers: [ @@ -36,6 +39,13 @@ import { ZohoService } from './services/zoho'; PipedriveService, HubspotService, ], - exports: [SyncService], + exports: [ + SyncService, + ServiceRegistry, + WebhookService, + FieldMappingService, + LoggerService, + PrismaService, + ], }) export class DealModule {} diff --git a/packages/api/src/crm/deal/sync/sync.processor.ts b/packages/api/src/crm/deal/sync/sync.processor.ts new file mode 100644 index 000000000..edcc09b42 --- /dev/null +++ b/packages/api/src/crm/deal/sync/sync.processor.ts @@ -0,0 +1,17 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; + +@Processor('syncTasks') +export class SyncProcessor { + constructor(private syncService: SyncService) {} + @Process('crm-sync-deals') + async handleSyncDeals(job: Job) { + try { + console.log(`Processing queue -> crm-sync-deals ${job.id}`); + await this.syncService.syncDeals(); + } catch (error) { + console.error('Error syncing crm deals', error); + } + } +} diff --git a/packages/api/src/crm/deal/sync/sync.service.ts b/packages/api/src/crm/deal/sync/sync.service.ts index cf38c5059..13565fb4b 100644 --- a/packages/api/src/crm/deal/sync/sync.service.ts +++ b/packages/api/src/crm/deal/sync/sync.service.ts @@ -15,6 +15,8 @@ import { IDealService } from '../types'; import { OriginalDealOutput } from '@@core/utils/types/original/original.crm'; import { crm_deals as CrmDeal } from '@prisma/client'; import { CRM_PROVIDERS } from '@panora/shared'; +import { InjectQueue } from '@nestjs/bull'; +import { Queue } from 'bull'; @Injectable() export class SyncService implements OnModuleInit { @@ -24,25 +26,54 @@ export class SyncService implements OnModuleInit { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + @InjectQueue('syncTasks') private syncQueue: Queue, ) { this.logger.setContext(SyncService.name); } async onModuleInit() { try { - await this.syncDeals(); + await this.scheduleSyncJob(); } catch (error) { handleServiceError(error, this.logger); } } - @Cron('*/20 * * * *') + private async scheduleSyncJob() { + const jobName = 'crm-sync-deals'; + + // Remove existing jobs to avoid duplicates in case of application restart + const jobs = await this.syncQueue.getRepeatableJobs(); + for (const job of jobs) { + if (job.name === jobName) { + await this.syncQueue.removeRepeatableByKey(job.key); + } + } + // Add new job to the queue with a CRON expression + await this.syncQueue.add( + jobName, + {}, + { + repeat: { cron: '0 0 * * *' }, // Runs once a day at midnight + }, + ); + } //function used by sync worker which populate our crm_deals table //its role is to fetch all deals from providers 3rd parties and save the info inside our db - async syncDeals() { + //@Cron('*/2 * * * *') // every 2 minutes (for testing) + @Cron('0 */8 * * *') // every 8 hours + async syncDeals(user_id?: string) { try { this.logger.log(`Syncing deals....`); - const users = await this.prisma.users.findMany(); + const users = user_id + ? [ + await this.prisma.users.findUnique({ + where: { + id_user: user_id, + }, + }), + ] + : await this.prisma.users.findMany(); if (users && users.length > 0) { for (const user of users) { const projects = await this.prisma.projects.findMany({ diff --git a/packages/api/src/crm/deal/types/model.unified.ts b/packages/api/src/crm/deal/types/model.unified.ts index 27a4b4854..1a7c86767 100644 --- a/packages/api/src/crm/deal/types/model.unified.ts +++ b/packages/api/src/crm/deal/types/model.unified.ts @@ -1,26 +1,41 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsNumber, IsOptional, IsString, IsUUID } from 'class-validator'; export class UnifiedDealInput { - @ApiProperty({ description: 'The name of the deal' }) + @ApiProperty({ type: String, description: 'The name of the deal' }) + @IsString() name: string; - @ApiProperty({ description: 'The description of the deal' }) + @ApiProperty({ type: String, description: 'The description of the deal' }) + @IsString() description: string; - @ApiProperty({ description: 'The amount of the deal' }) + @ApiProperty({ type: Number, description: 'The amount of the deal' }) + @IsNumber() amount: number; @ApiPropertyOptional({ + type: String, description: 'The uuid of the user who is on the deal', }) + @IsUUID() + @IsOptional() user_id?: string; - @ApiPropertyOptional({ description: 'The uuid of the stage of the deal' }) + @ApiPropertyOptional({ + type: String, + description: 'The uuid of the stage of the deal', + }) + @IsUUID() + @IsOptional() stage_id?: string; @ApiPropertyOptional({ + type: String, description: 'The uuid of the company tied to the deal', }) + @IsUUID() + @IsOptional() company_id?: string; @ApiPropertyOptional({ @@ -28,22 +43,29 @@ export class UnifiedDealInput { description: 'The custom field mappings of the company between the remote 3rd party & Panora', }) + @IsOptional() field_mappings?: Record; } export class UnifiedDealOutput extends UnifiedDealInput { - @ApiPropertyOptional({ description: 'The uuid of the deal' }) + @ApiPropertyOptional({ type: String, description: 'The uuid of the deal' }) + @IsUUID() + @IsOptional() id?: string; @ApiPropertyOptional({ + type: String, description: 'The id of the deal in the context of the Crm 3rd Party', }) + @IsString() + @IsOptional() remote_id?: string; @ApiPropertyOptional({ - type: [{}], + type: {}, description: 'The remote data of the deal in the context of the Crm 3rd Party', }) + @IsOptional() remote_data?: Record; } diff --git a/packages/api/src/crm/engagement/engagement.module.ts b/packages/api/src/crm/engagement/engagement.module.ts index d6b7ad081..1f068bf0e 100644 --- a/packages/api/src/crm/engagement/engagement.module.ts +++ b/packages/api/src/crm/engagement/engagement.module.ts @@ -16,9 +16,12 @@ import { ZohoService } from './services/zoho'; @Module({ imports: [ - BullModule.registerQueue({ - name: 'webhookDelivery', - }), + BullModule.registerQueue( + { + name: 'webhookDelivery', + }, + { name: 'syncTasks' }, + ), ], controllers: [EngagementController], providers: [ @@ -36,6 +39,13 @@ import { ZohoService } from './services/zoho'; PipedriveService, HubspotService, ], - exports: [SyncService], + exports: [ + SyncService, + ServiceRegistry, + WebhookService, + FieldMappingService, + LoggerService, + PrismaService, + ], }) export class EngagementModule {} diff --git a/packages/api/src/crm/engagement/sync/sync.processor.ts b/packages/api/src/crm/engagement/sync/sync.processor.ts new file mode 100644 index 000000000..2359a9933 --- /dev/null +++ b/packages/api/src/crm/engagement/sync/sync.processor.ts @@ -0,0 +1,17 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; + +@Processor('syncTasks') +export class SyncProcessor { + constructor(private syncService: SyncService) {} + @Process('crm-sync-engagements') + async handleSyncEngagements(job: Job) { + try { + console.log(`Processing queue -> crm-sync-engagements ${job.id}`); + await this.syncService.syncEngagements(); + } catch (error) { + console.error('Error syncing crm engagements', error); + } + } +} diff --git a/packages/api/src/crm/engagement/sync/sync.service.ts b/packages/api/src/crm/engagement/sync/sync.service.ts index 05bb08ada..68c1bd0dc 100644 --- a/packages/api/src/crm/engagement/sync/sync.service.ts +++ b/packages/api/src/crm/engagement/sync/sync.service.ts @@ -16,6 +16,8 @@ import { crm_engagements as CrmEngagement } from '@prisma/client'; import { OriginalEngagementOutput } from '@@core/utils/types/original/original.crm'; import { ENGAGEMENTS_TYPE } from '../utils'; import { CRM_PROVIDERS } from '@panora/shared'; +import { InjectQueue } from '@nestjs/bull'; +import { Queue } from 'bull'; @Injectable() export class SyncService implements OnModuleInit { @@ -25,26 +27,54 @@ export class SyncService implements OnModuleInit { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + @InjectQueue('syncTasks') private syncQueue: Queue, ) { this.logger.setContext(SyncService.name); } async onModuleInit() { try { - //TODO: to test after - //await this.syncEngagements(); + await this.scheduleSyncJob(); } catch (error) { handleServiceError(error, this.logger); } } - @Cron('*/20 * * * *') + private async scheduleSyncJob() { + const jobName = 'crm-sync-engagements'; + + // Remove existing jobs to avoid duplicates in case of application restart + const jobs = await this.syncQueue.getRepeatableJobs(); + for (const job of jobs) { + if (job.name === jobName) { + await this.syncQueue.removeRepeatableByKey(job.key); + } + } + // Add new job to the queue with a CRON expression + await this.syncQueue.add( + jobName, + {}, + { + repeat: { cron: '0 0 * * *' }, // Runs once a day at midnight + }, + ); + } //function used by sync worker which populate our crm_engagements table //its role is to fetch all engagements from providers 3rd parties and save the info inside our db - async syncEngagements() { + //@Cron('*/2 * * * *') // every 2 minutes (for testing) + @Cron('0 */8 * * *') // every 8 hours + async syncEngagements(user_id?: string) { try { this.logger.log(`Syncing engagements....`); - const users = await this.prisma.users.findMany(); + const users = user_id + ? [ + await this.prisma.users.findUnique({ + where: { + id_user: user_id, + }, + }), + ] + : await this.prisma.users.findMany(); if (users && users.length > 0) { for (const user of users) { const projects = await this.prisma.projects.findMany({ diff --git a/packages/api/src/crm/engagement/types/model.unified.ts b/packages/api/src/crm/engagement/types/model.unified.ts index 0008e23b1..40fb989d2 100644 --- a/packages/api/src/crm/engagement/types/model.unified.ts +++ b/packages/api/src/crm/engagement/types/model.unified.ts @@ -1,40 +1,73 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsIn, IsOptional, IsString, IsUUID } from 'class-validator'; export class UnifiedEngagementInput { - @ApiPropertyOptional({ description: 'The content of the engagement' }) + @ApiPropertyOptional({ + type: String, + description: 'The content of the engagement', + }) + @IsString() + @IsOptional() content?: string; - @ApiPropertyOptional({ description: 'The direction of the engagement' }) + @ApiPropertyOptional({ + type: String, + description: + 'The direction of the engagement. Authorized values are INBOUND or OUTBOUND', + }) + @IsIn(['INBOUND', 'OUTBOUND'], { + message: 'Direction must be either INBOUND or OUTBOUND', + }) + @IsOptional() direction?: string; - @ApiPropertyOptional({ description: 'The subject of the engagement' }) + @ApiPropertyOptional({ + type: String, + description: 'The subject of the engagement', + }) + @IsString() + @IsOptional() subject?: string; @ApiPropertyOptional({ description: 'The start time of the engagement' }) + @IsOptional() start_at?: Date; @ApiPropertyOptional({ description: 'The end time of the engagement' }) + @IsOptional() end_time?: Date; @ApiProperty({ + type: String, description: 'The type of the engagement. Authorized values are EMAIL, CALL or MEETING', }) + @IsIn(['EMAIL', 'CALL', 'MEETING'], { + message: 'Type must be either EMAIL, CALL or MEETING', + }) type: string; @ApiPropertyOptional({ + type: String, description: 'The uuid of the user tied to the engagement', }) + @IsUUID() + @IsOptional() user_id?: string; @ApiPropertyOptional({ + type: String, description: 'The uuid of the company tied to the engagement', }) + @IsUUID() + @IsOptional() company_id?: string; // uuid of Company object @ApiPropertyOptional({ + type: [String], description: 'The uuids of contacts tied to the engagement object', }) + @IsOptional() contacts?: string[]; // array of uuids of Engagement Contacts objects @ApiPropertyOptional({ @@ -42,22 +75,32 @@ export class UnifiedEngagementInput { description: 'The custom field mappings of the engagement between the remote 3rd party & Panora', }) + @IsOptional() field_mappings?: Record; } export class UnifiedEngagementOutput extends UnifiedEngagementInput { - @ApiPropertyOptional({ description: 'The uuid of the engagement' }) + @ApiPropertyOptional({ + type: String, + description: 'The uuid of the engagement', + }) + @IsUUID() + @IsOptional() id?: string; @ApiPropertyOptional({ + type: String, description: 'The id of the engagement in the context of the Crm 3rd Party', }) + @IsString() + @IsOptional() remote_id?: string; @ApiPropertyOptional({ - type: [{}], + type: {}, description: 'The remote data of the engagement in the context of the Crm 3rd Party', }) + @IsOptional() remote_data?: Record; } diff --git a/packages/api/src/crm/note/note.module.ts b/packages/api/src/crm/note/note.module.ts index 24db7f527..1c2de8847 100644 --- a/packages/api/src/crm/note/note.module.ts +++ b/packages/api/src/crm/note/note.module.ts @@ -16,9 +16,12 @@ import { ZohoService } from './services/zoho'; @Module({ imports: [ - BullModule.registerQueue({ - name: 'webhookDelivery', - }), + BullModule.registerQueue( + { + name: 'webhookDelivery', + }, + { name: 'syncTasks' }, + ), ], controllers: [NoteController], providers: [ @@ -36,6 +39,13 @@ import { ZohoService } from './services/zoho'; PipedriveService, HubspotService, ], - exports: [SyncService], + exports: [ + SyncService, + ServiceRegistry, + WebhookService, + FieldMappingService, + LoggerService, + PrismaService, + ], }) export class NoteModule {} diff --git a/packages/api/src/crm/note/sync/sync.processor.ts b/packages/api/src/crm/note/sync/sync.processor.ts new file mode 100644 index 000000000..002e5f562 --- /dev/null +++ b/packages/api/src/crm/note/sync/sync.processor.ts @@ -0,0 +1,17 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; + +@Processor('syncTasks') +export class SyncProcessor { + constructor(private syncService: SyncService) {} + @Process('crm-sync-notes') + async handleSyncNotes(job: Job) { + try { + console.log(`Processing queue -> crm-sync-notes ${job.id}`); + await this.syncService.syncNotes(); + } catch (error) { + console.error('Error syncing crm notes', error); + } + } +} diff --git a/packages/api/src/crm/note/sync/sync.service.ts b/packages/api/src/crm/note/sync/sync.service.ts index 237790323..09d98ce19 100644 --- a/packages/api/src/crm/note/sync/sync.service.ts +++ b/packages/api/src/crm/note/sync/sync.service.ts @@ -15,6 +15,8 @@ import { INoteService } from '../types'; import { crm_notes as CrmNote } from '@prisma/client'; import { OriginalNoteOutput } from '@@core/utils/types/original/original.crm'; import { CRM_PROVIDERS } from '@panora/shared'; +import { InjectQueue } from '@nestjs/bull'; +import { Queue } from 'bull'; @Injectable() export class SyncService implements OnModuleInit { @@ -24,25 +26,54 @@ export class SyncService implements OnModuleInit { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + @InjectQueue('syncTasks') private syncQueue: Queue, ) { this.logger.setContext(SyncService.name); } async onModuleInit() { try { - await this.syncNotes(); + await this.scheduleSyncJob(); } catch (error) { handleServiceError(error, this.logger); } } - @Cron('*/20 * * * *') + private async scheduleSyncJob() { + const jobName = 'crm-sync-notes'; + + // Remove existing jobs to avoid duplicates in case of application restart + const jobs = await this.syncQueue.getRepeatableJobs(); + for (const job of jobs) { + if (job.name === jobName) { + await this.syncQueue.removeRepeatableByKey(job.key); + } + } + // Add new job to the queue with a CRON expression + await this.syncQueue.add( + jobName, + {}, + { + repeat: { cron: '0 0 * * *' }, // Runs once a day at midnight + }, + ); + } //function used by sync worker which populate our crm_notes table //its role is to fetch all notes from providers 3rd parties and save the info inside our db - async syncNotes() { + //@Cron('*/2 * * * *') // every 2 minutes (for testing) + @Cron('0 */8 * * *') // every 8 hours + async syncNotes(user_id?: string) { try { this.logger.log(`Syncing notes....`); - const users = await this.prisma.users.findMany(); + const users = user_id + ? [ + await this.prisma.users.findUnique({ + where: { + id_user: user_id, + }, + }), + ] + : await this.prisma.users.findMany(); if (users && users.length > 0) { for (const user of users) { const projects = await this.prisma.projects.findMany({ diff --git a/packages/api/src/crm/note/types/model.unified.ts b/packages/api/src/crm/note/types/model.unified.ts index 8658020e3..1a6fb3fda 100644 --- a/packages/api/src/crm/note/types/model.unified.ts +++ b/packages/api/src/crm/note/types/model.unified.ts @@ -1,23 +1,41 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsOptional, IsString, IsUUID } from 'class-validator'; export class UnifiedNoteInput { - @ApiProperty({ description: 'The content of the note' }) + @ApiProperty({ type: String, description: 'The content of the note' }) + @IsString() content: string; - @ApiPropertyOptional({ description: 'The uuid of the user tied the note' }) + @ApiPropertyOptional({ + type: String, + description: 'The uuid of the user tied the note', + }) + @IsUUID() + @IsOptional() user_id?: string; @ApiPropertyOptional({ + type: String, description: 'The uuid of the company tied to the note', }) + @IsUUID() + @IsOptional() company_id?: string; @ApiPropertyOptional({ + type: String, description: 'The uuid fo the contact tied to the note', }) + @IsUUID() + @IsOptional() contact_id?: string; - @ApiPropertyOptional({ description: 'The uuid of the deal tied to the note' }) + @ApiPropertyOptional({ + type: String, + description: 'The uuid of the deal tied to the note', + }) + @IsUUID() + @IsOptional() deal_id?: string; @ApiPropertyOptional({ @@ -25,22 +43,30 @@ export class UnifiedNoteInput { description: 'The custom field mappings of the note between the remote 3rd party & Panora', }) + @IsOptional() field_mappings?: Record; } export class UnifiedNoteOutput extends UnifiedNoteInput { - @ApiPropertyOptional({ description: 'The uuid of the note' }) + @ApiPropertyOptional({ type: String, description: 'The uuid of the note' }) + @IsUUID() + @IsOptional() id?: string; @ApiPropertyOptional({ + type: String, + description: 'The id of the note in the context of the Crm 3rd Party', }) + @IsString() + @IsOptional() remote_id?: string; @ApiPropertyOptional({ - type: [{}], + type: {}, description: 'The remote data of the note in the context of the Crm 3rd Party', }) + @IsOptional() remote_data?: Record; } diff --git a/packages/api/src/crm/stage/stage.module.ts b/packages/api/src/crm/stage/stage.module.ts index 6376d5286..3105933f1 100644 --- a/packages/api/src/crm/stage/stage.module.ts +++ b/packages/api/src/crm/stage/stage.module.ts @@ -16,9 +16,12 @@ import { ZohoService } from './services/zoho'; @Module({ imports: [ - BullModule.registerQueue({ - name: 'webhookDelivery', - }), + BullModule.registerQueue( + { + name: 'webhookDelivery', + }, + { name: 'syncTasks' }, + ), ], controllers: [StageController], providers: [ @@ -36,6 +39,13 @@ import { ZohoService } from './services/zoho'; PipedriveService, HubspotService, ], - exports: [SyncService], + exports: [ + SyncService, + ServiceRegistry, + WebhookService, + FieldMappingService, + LoggerService, + PrismaService, + ], }) export class StageModule {} diff --git a/packages/api/src/crm/stage/sync/sync.processor.ts b/packages/api/src/crm/stage/sync/sync.processor.ts new file mode 100644 index 000000000..403915529 --- /dev/null +++ b/packages/api/src/crm/stage/sync/sync.processor.ts @@ -0,0 +1,17 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; + +@Processor('syncTasks') +export class SyncProcessor { + constructor(private syncService: SyncService) {} + @Process('crm-sync-stages') + async handleSyncStages(job: Job) { + try { + console.log(`Processing queue -> crm-sync-stages ${job.id}`); + await this.syncService.syncStages(); + } catch (error) { + console.error('Error syncing crm stages', error); + } + } +} diff --git a/packages/api/src/crm/stage/sync/sync.service.ts b/packages/api/src/crm/stage/sync/sync.service.ts index 344f18657..3819c18aa 100644 --- a/packages/api/src/crm/stage/sync/sync.service.ts +++ b/packages/api/src/crm/stage/sync/sync.service.ts @@ -15,6 +15,8 @@ import { IStageService } from '../types'; import { crm_deals_stages as CrmStage } from '@prisma/client'; import { OriginalStageOutput } from '@@core/utils/types/original/original.crm'; import { CRM_PROVIDERS } from '@panora/shared'; +import { InjectQueue } from '@nestjs/bull'; +import { Queue } from 'bull'; @Injectable() export class SyncService implements OnModuleInit { @@ -24,25 +26,54 @@ export class SyncService implements OnModuleInit { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + @InjectQueue('syncTasks') private syncQueue: Queue, ) { this.logger.setContext(SyncService.name); } async onModuleInit() { try { - await this.syncStages(); + await this.scheduleSyncJob(); } catch (error) { handleServiceError(error, this.logger); } } - @Cron('*/20 * * * *') + private async scheduleSyncJob() { + const jobName = 'crm-sync-stages'; + + // Remove existing jobs to avoid duplicates in case of application restart + const jobs = await this.syncQueue.getRepeatableJobs(); + for (const job of jobs) { + if (job.name === jobName) { + await this.syncQueue.removeRepeatableByKey(job.key); + } + } + // Add new job to the queue with a CRON expression + await this.syncQueue.add( + jobName, + {}, + { + repeat: { cron: '0 0 * * *' }, // Runs once a day at midnight + }, + ); + } //function used by sync worker which populate our crm_stages table //its role is to fetch all stages from providers 3rd parties and save the info inside our db - async syncStages() { + //@Cron('*/2 * * * *') // every 2 minutes (for testing) + @Cron('0 */8 * * *') // every 8 hours + async syncStages(user_id?: string) { try { this.logger.log(`Syncing stages....`); - const users = await this.prisma.users.findMany(); + const users = user_id + ? [ + await this.prisma.users.findUnique({ + where: { + id_user: user_id, + }, + }), + ] + : await this.prisma.users.findMany(); if (users && users.length > 0) { for (const user of users) { const projects = await this.prisma.projects.findMany({ diff --git a/packages/api/src/crm/stage/types/model.unified.ts b/packages/api/src/crm/stage/types/model.unified.ts index 3b5bc408b..96831e68c 100644 --- a/packages/api/src/crm/stage/types/model.unified.ts +++ b/packages/api/src/crm/stage/types/model.unified.ts @@ -1,7 +1,9 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsOptional, IsString, IsUUID } from 'class-validator'; export class UnifiedStageInput { - @ApiProperty({ description: 'The name of the stage' }) + @ApiProperty({ type: String, description: 'The name of the stage' }) + @IsString() stage_name: string; @ApiPropertyOptional({ @@ -9,22 +11,29 @@ export class UnifiedStageInput { description: 'The custom field mappings of the stage between the remote 3rd party & Panora', }) + @IsOptional() field_mappings?: Record; } export class UnifiedStageOutput extends UnifiedStageInput { - @ApiPropertyOptional({ description: 'The uuid of the stage' }) + @ApiPropertyOptional({ type: String, description: 'The uuid of the stage' }) + @IsUUID() + @IsOptional() id?: string; @ApiPropertyOptional({ + type: String, description: 'The id of the stage in the context of the Crm 3rd Party', }) + @IsString() + @IsOptional() remote_id?: string; @ApiPropertyOptional({ - type: [{}], + type: {}, description: 'The remote data of the stage in the context of the Crm 3rd Party', }) + @IsOptional() remote_data?: Record; } diff --git a/packages/api/src/crm/task/services/hubspot/mappers.ts b/packages/api/src/crm/task/services/hubspot/mappers.ts index c8b22923d..eac091fe5 100644 --- a/packages/api/src/crm/task/services/hubspot/mappers.ts +++ b/packages/api/src/crm/task/services/hubspot/mappers.ts @@ -23,7 +23,7 @@ export class HubspotTaskMapper implements ITaskMapper { const result: HubspotTaskInput = { hs_task_subject: source.subject || '', hs_task_body: source.content || '', - hs_task_status: this.mapStatus(source.status), + hs_task_status: source.status, hs_task_priority: '', hs_timestamp: source.due_date ? source.due_date.toISOString() @@ -97,42 +97,11 @@ export class HubspotTaskMapper implements ITaskMapper { return { subject: task.properties.hs_task_subject, content: task.properties.hs_task_body, - status: this.mapStatusReverse(task.properties.hs_task_status), + status: task.properties.hs_task_status, due_date: new Date(task.properties.hs_timestamp), field_mappings, ...opts, // Additional fields mapping based on UnifiedTaskOutput structure }; } - - private mapStatus(status?: string): string { - // Map UnifiedTaskInput status to HubspotTaskInput status - // Adjust this method according to your specific status mapping logic - return status || 'WAITING'; - } - - private mapPriority(priority?: string): 'HIGH' | 'MEDIUM' | 'LOW' { - // Map UnifiedTaskInput priority to HubspotTaskInput priority - // Adjust this method according to your specific priority mapping logic - return priority === 'High' - ? 'HIGH' - : priority === 'Medium' - ? 'MEDIUM' - : 'LOW'; - } - - private mapStatusReverse(status: string): string { - // Reverse map HubspotTaskOutput status to UnifiedTaskOutput status - // Adjust this method according to your specific status reverse mapping logic - switch (status) { - case 'WAITING': - return 'Pending'; - case 'COMPLETED': - return 'Completed'; - case 'IN_PROGRESS': - return 'In Progress'; - default: - return 'Unknown'; - } - } } diff --git a/packages/api/src/crm/task/sync/sync.processor.ts b/packages/api/src/crm/task/sync/sync.processor.ts new file mode 100644 index 000000000..f0e17540c --- /dev/null +++ b/packages/api/src/crm/task/sync/sync.processor.ts @@ -0,0 +1,17 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; + +@Processor('syncTasks') +export class SyncProcessor { + constructor(private syncService: SyncService) {} + @Process('crm-sync-tasks') + async handleSyncTasks(job: Job) { + try { + console.log(`Processing queue -> crm-sync-tasks ${job.id}`); + await this.syncService.syncTasks(); + } catch (error) { + console.error('Error syncing crm tasks', error); + } + } +} diff --git a/packages/api/src/crm/task/sync/sync.service.ts b/packages/api/src/crm/task/sync/sync.service.ts index ddc5a0e76..65c3b14f7 100644 --- a/packages/api/src/crm/task/sync/sync.service.ts +++ b/packages/api/src/crm/task/sync/sync.service.ts @@ -15,6 +15,8 @@ import { ITaskService } from '../types'; import { crm_tasks as CrmTask } from '@prisma/client'; import { OriginalTaskOutput } from '@@core/utils/types/original/original.crm'; import { CRM_PROVIDERS } from '@panora/shared'; +import { InjectQueue } from '@nestjs/bull'; +import { Queue } from 'bull'; @Injectable() export class SyncService implements OnModuleInit { @@ -24,25 +26,54 @@ export class SyncService implements OnModuleInit { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + @InjectQueue('syncTasks') private syncQueue: Queue, ) { this.logger.setContext(SyncService.name); } async onModuleInit() { try { - await this.syncTasks(); + await this.scheduleSyncJob(); } catch (error) { handleServiceError(error, this.logger); } } - @Cron('*/20 * * * *') + private async scheduleSyncJob() { + const jobName = 'crm-sync-tasks'; + + // Remove existing jobs to avoid duplicates in case of application restart + const jobs = await this.syncQueue.getRepeatableJobs(); + for (const job of jobs) { + if (job.name === jobName) { + await this.syncQueue.removeRepeatableByKey(job.key); + } + } + // Add new job to the queue with a CRON expression + await this.syncQueue.add( + jobName, + {}, + { + repeat: { cron: '0 0 * * *' }, // Runs once a day at midnight + }, + ); + } //function used by sync worker which populate our crm_tasks table //its role is to fetch all tasks from providers 3rd parties and save the info inside our db - async syncTasks() { + //@Cron('*/2 * * * *') // every 2 minutes (for testing) + @Cron('0 */8 * * *') // every 8 hours + async syncTasks(user_id?: string) { try { this.logger.log(`Syncing tasks....`); - const users = await this.prisma.users.findMany(); + const users = user_id + ? [ + await this.prisma.users.findUnique({ + where: { + id_user: user_id, + }, + }), + ] + : await this.prisma.users.findMany(); if (users && users.length > 0) { for (const user of users) { const projects = await this.prisma.projects.findMany({ diff --git a/packages/api/src/crm/task/task.module.ts b/packages/api/src/crm/task/task.module.ts index e30347cad..e392a5f94 100644 --- a/packages/api/src/crm/task/task.module.ts +++ b/packages/api/src/crm/task/task.module.ts @@ -16,9 +16,12 @@ import { ZohoService } from './services/zoho'; @Module({ imports: [ - BullModule.registerQueue({ - name: 'webhookDelivery', - }), + BullModule.registerQueue( + { + name: 'webhookDelivery', + }, + { name: 'syncTasks' }, + ), ], controllers: [TaskController], providers: [ @@ -36,6 +39,13 @@ import { ZohoService } from './services/zoho'; PipedriveService, HubspotService, ], - exports: [SyncService], + exports: [ + SyncService, + ServiceRegistry, + WebhookService, + FieldMappingService, + LoggerService, + PrismaService, + ], }) export class TaskModule {} diff --git a/packages/api/src/crm/task/types/model.unified.ts b/packages/api/src/crm/task/types/model.unified.ts index fb4abc11b..d3e278676 100644 --- a/packages/api/src/crm/task/types/model.unified.ts +++ b/packages/api/src/crm/task/types/model.unified.ts @@ -1,33 +1,55 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsIn, IsOptional, IsString, IsUUID } from 'class-validator'; export class UnifiedTaskInput { - @ApiProperty({ description: 'The subject of the task' }) + @ApiProperty({ type: String, description: 'The subject of the task' }) + @IsString() subject: string; - @ApiProperty({ description: 'The content of the task' }) + @ApiProperty({ type: String, description: 'The content of the task' }) + @IsString() content: string; @ApiProperty({ + type: String, description: - 'The status of the task. Authorized values are "Completed" and "Not Completed" ', + 'The status of the task. Authorized values are PENDING, COMPLETED.', + }) + @IsIn(['PENDING', 'COMPLETED'], { + message: 'Type must be either PENDING or COMPLETED', }) status: string; @ApiPropertyOptional({ description: 'The due date of the task' }) + @IsOptional() due_date?: Date; @ApiPropertyOptional({ description: 'The finished date of the task' }) + @IsOptional() finished_date?: Date; - @ApiPropertyOptional({ description: 'The uuid of the user tied to the task' }) + @ApiPropertyOptional({ + type: String, + description: 'The uuid of the user tied to the task', + }) + @IsUUID() + @IsOptional() user_id?: string; @ApiPropertyOptional({ + type: String, description: 'The uuid fo the company tied to the task', }) + @IsUUID() + @IsOptional() company_id?: string; - @ApiPropertyOptional({ description: 'The uuid of the deal tied to the task' }) + @ApiPropertyOptional({ + type: String, + description: 'The uuid of the deal tied to the task', + }) + @IsString() + @IsOptional() deal_id?: string; @ApiPropertyOptional({ @@ -35,22 +57,30 @@ export class UnifiedTaskInput { description: 'The custom field mappings of the task between the remote 3rd party & Panora', }) + @IsOptional() field_mappings?: Record; } export class UnifiedTaskOutput extends UnifiedTaskInput { - @ApiPropertyOptional({ description: 'The uuid of the task' }) + @ApiPropertyOptional({ type: String, description: 'The uuid of the task' }) + @IsUUID() + @IsOptional() id?: string; @ApiPropertyOptional({ + type: String, + description: 'The id of the task in the context of the Crm 3rd Party', }) + @IsString() + @IsOptional() remote_id?: string; @ApiPropertyOptional({ - type: [{}], + type: {}, description: 'The remote data of the task in the context of the Crm 3rd Party', }) + @IsOptional() remote_data?: Record; } diff --git a/packages/api/src/crm/task/utils/index.ts b/packages/api/src/crm/task/utils/index.ts index fffeb8f1d..d2437f469 100644 --- a/packages/api/src/crm/task/utils/index.ts +++ b/packages/api/src/crm/task/utils/index.ts @@ -134,4 +134,26 @@ export class Utils { throw new Error(error); } } + + mapStatus(status: string, provider_name: string): string { + try { + switch (provider_name.toLowerCase()) { + default: + throw new Error( + 'Provider not supported for status custom task mapping', + ); + } + } catch (error) { + throw new Error(error); + } + } + + // not currently in use, but might be in the future + mapPriority(priority?: string): 'HIGH' | 'MEDIUM' | 'LOW' { + return priority === 'High' + ? 'HIGH' + : priority === 'Medium' + ? 'MEDIUM' + : 'LOW'; + } } diff --git a/packages/api/src/crm/user/sync/sync.processor.ts b/packages/api/src/crm/user/sync/sync.processor.ts new file mode 100644 index 000000000..d48368a23 --- /dev/null +++ b/packages/api/src/crm/user/sync/sync.processor.ts @@ -0,0 +1,17 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; + +@Processor('syncTasks') +export class SyncProcessor { + constructor(private syncService: SyncService) {} + @Process('crm-sync-users') + async handleSyncUsers(job: Job) { + try { + console.log(`Processing queue -> crm-sync-users ${job.id}`); + await this.syncService.syncUsers(); + } catch (error) { + console.error('Error syncing crm users', error); + } + } +} diff --git a/packages/api/src/crm/user/sync/sync.service.ts b/packages/api/src/crm/user/sync/sync.service.ts index af19920ac..c7b552f8b 100644 --- a/packages/api/src/crm/user/sync/sync.service.ts +++ b/packages/api/src/crm/user/sync/sync.service.ts @@ -15,6 +15,9 @@ import { IUserService } from '../types'; import { crm_users as CrmUser } from '@prisma/client'; import { OriginalUserOutput } from '@@core/utils/types/original/original.crm'; import { CRM_PROVIDERS } from '@panora/shared'; +import { InjectQueue } from '@nestjs/bull'; +import { Queue } from 'bull'; + @Injectable() export class SyncService implements OnModuleInit { constructor( @@ -23,25 +26,54 @@ export class SyncService implements OnModuleInit { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + @InjectQueue('syncTasks') private syncQueue: Queue, ) { this.logger.setContext(SyncService.name); } async onModuleInit() { try { - await this.syncUsers(); + await this.scheduleSyncJob(); } catch (error) { handleServiceError(error, this.logger); } } - @Cron('*/20 * * * *') + private async scheduleSyncJob() { + const jobName = 'crm-sync-users'; + + // Remove existing jobs to avoid duplicates in case of application restart + const jobs = await this.syncQueue.getRepeatableJobs(); + for (const job of jobs) { + if (job.name === jobName) { + await this.syncQueue.removeRepeatableByKey(job.key); + } + } + // Add new job to the queue with a CRON expression + await this.syncQueue.add( + jobName, + {}, + { + repeat: { cron: '0 0 * * *' }, // Runs once a day at midnight + }, + ); + } //function used by sync worker which populate our crm_users table //its role is to fetch all users from providers 3rd parties and save the info inside our db - async syncUsers() { + //@Cron('*/2 * * * *') // every 2 minutes (for testing) + @Cron('0 */8 * * *') // every 8 hours + async syncUsers(user_id?: string) { try { this.logger.log(`Syncing users....`); - const users = await this.prisma.users.findMany(); + const users = user_id + ? [ + await this.prisma.users.findUnique({ + where: { + id_user: user_id, + }, + }), + ] + : await this.prisma.users.findMany(); if (users && users.length > 0) { for (const user of users) { const projects = await this.prisma.projects.findMany({ diff --git a/packages/api/src/crm/user/types/model.unified.ts b/packages/api/src/crm/user/types/model.unified.ts index 71dee8151..0758a8598 100644 --- a/packages/api/src/crm/user/types/model.unified.ts +++ b/packages/api/src/crm/user/types/model.unified.ts @@ -1,10 +1,13 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsOptional, IsString, IsUUID } from 'class-validator'; export class UnifiedUserInput { - @ApiProperty({ description: 'The name of the user' }) + @ApiProperty({ type: String, description: 'The name of the user' }) + @IsString() name: string; - @ApiProperty({ description: 'The email of the user' }) + @ApiProperty({ type: String, description: 'The email of the user' }) + @IsString() email: string; @ApiPropertyOptional({ @@ -12,22 +15,29 @@ export class UnifiedUserInput { description: 'The custom field mappings of the user between the remote 3rd party & Panora', }) + @IsOptional() field_mappings?: Record; } export class UnifiedUserOutput extends UnifiedUserInput { - @ApiPropertyOptional({ description: 'The uuid of the user' }) + @ApiPropertyOptional({ type: String, description: 'The uuid of the user' }) + @IsUUID() + @IsOptional() id?: string; @ApiPropertyOptional({ + type: String, description: 'The id of the user in the context of the Crm 3rd Party', }) + @IsString() + @IsOptional() remote_id?: string; @ApiPropertyOptional({ - type: [{}], + type: {}, description: 'The remote data of the user in the context of the Crm 3rd Party', }) + @IsOptional() remote_data?: Record; } diff --git a/packages/api/src/crm/user/user.module.ts b/packages/api/src/crm/user/user.module.ts index 5ddfd8b7e..46960ac91 100644 --- a/packages/api/src/crm/user/user.module.ts +++ b/packages/api/src/crm/user/user.module.ts @@ -16,9 +16,12 @@ import { ZohoService } from './services/zoho'; @Module({ imports: [ - BullModule.registerQueue({ - name: 'webhookDelivery', - }), + BullModule.registerQueue( + { + name: 'webhookDelivery', + }, + { name: 'syncTasks' }, + ), ], controllers: [UserController], providers: [ @@ -36,6 +39,13 @@ import { ZohoService } from './services/zoho'; PipedriveService, HubspotService, ], - exports: [SyncService], + exports: [ + SyncService, + ServiceRegistry, + WebhookService, + FieldMappingService, + LoggerService, + PrismaService, + ], }) export class UserModule {} diff --git a/packages/api/src/ticketing/account/account.module.ts b/packages/api/src/ticketing/account/account.module.ts index b8c7862f7..cd34201d9 100644 --- a/packages/api/src/ticketing/account/account.module.ts +++ b/packages/api/src/ticketing/account/account.module.ts @@ -14,9 +14,12 @@ import { FrontService } from './services/front'; @Module({ imports: [ - BullModule.registerQueue({ - name: 'webhookDelivery', - }), + BullModule.registerQueue( + { + name: 'webhookDelivery', + }, + { name: 'syncTasks' }, + ), ], controllers: [AccountController], providers: [ @@ -32,6 +35,13 @@ import { FrontService } from './services/front'; ZendeskService, FrontService, ], - exports: [SyncService], + exports: [ + SyncService, + ServiceRegistry, + WebhookService, + FieldMappingService, + LoggerService, + PrismaService, + ], }) export class AccountModule {} diff --git a/packages/api/src/ticketing/account/sync/sync.processor.ts b/packages/api/src/ticketing/account/sync/sync.processor.ts new file mode 100644 index 000000000..325d66090 --- /dev/null +++ b/packages/api/src/ticketing/account/sync/sync.processor.ts @@ -0,0 +1,17 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; + +@Processor('syncTasks') +export class SyncProcessor { + constructor(private syncService: SyncService) {} + @Process('ticketing-sync-accounts') + async handleSyncAccounts(job: Job) { + try { + console.log(`Processing queue -> ticketing-sync-accounts ${job.id}`); + await this.syncService.syncAccounts(); + } catch (error) { + console.error('Error syncing ticketing accounts', error); + } + } +} diff --git a/packages/api/src/ticketing/account/sync/sync.service.ts b/packages/api/src/ticketing/account/sync/sync.service.ts index eb9462bbb..5ec6d55ed 100644 --- a/packages/api/src/ticketing/account/sync/sync.service.ts +++ b/packages/api/src/ticketing/account/sync/sync.service.ts @@ -15,6 +15,8 @@ import { IAccountService } from '../types'; import { OriginalAccountOutput } from '@@core/utils/types/original/original.ticketing'; import { tcg_accounts as TicketingAccount } from '@prisma/client'; import { TICKETING_PROVIDERS } from '@panora/shared'; +import { InjectQueue } from '@nestjs/bull'; +import { Queue } from 'bull'; @Injectable() export class SyncService implements OnModuleInit { @@ -24,25 +26,54 @@ export class SyncService implements OnModuleInit { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + @InjectQueue('syncTasks') private syncQueue: Queue, ) { this.logger.setContext(SyncService.name); } async onModuleInit() { try { - await this.syncAccounts(); + await this.scheduleSyncJob(); } catch (error) { handleServiceError(error, this.logger); } } - @Cron('*/20 * * * *') + private async scheduleSyncJob() { + const jobName = 'ticketing-sync-accounts'; + + // Remove existing jobs to avoid duplicates in case of application restart + const jobs = await this.syncQueue.getRepeatableJobs(); + for (const job of jobs) { + if (job.name === jobName) { + await this.syncQueue.removeRepeatableByKey(job.key); + } + } + // Add new job to the queue with a CRON expression + await this.syncQueue.add( + jobName, + {}, + { + repeat: { cron: '0 0 * * *' }, // Runs once a day at midnight + }, + ); + } //function used by sync worker which populate our tcg_accounts table //its role is to fetch all accounts from providers 3rd parties and save the info inside our db - async syncAccounts() { + //@Cron('*/2 * * * *') // every 2 minutes (for testing) + @Cron('0 */8 * * *') // every 8 hours + async syncAccounts(user_id?: string) { try { this.logger.log(`Syncing accounts....`); - const users = await this.prisma.users.findMany(); + const users = user_id + ? [ + await this.prisma.users.findUnique({ + where: { + id_user: user_id, + }, + }), + ] + : await this.prisma.users.findMany(); if (users && users.length > 0) { for (const user of users) { const projects = await this.prisma.projects.findMany({ diff --git a/packages/api/src/ticketing/account/types/model.unified.ts b/packages/api/src/ticketing/account/types/model.unified.ts index 9652b2cb7..d0e70293b 100644 --- a/packages/api/src/ticketing/account/types/model.unified.ts +++ b/packages/api/src/ticketing/account/types/model.unified.ts @@ -1,13 +1,16 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsOptional, IsString, IsUUID } from 'class-validator'; export class UnifiedAccountInput { - @ApiProperty({ description: 'The name of the account' }) + @ApiProperty({ type: String, description: 'The name of the account' }) + @IsString() name: string; @ApiPropertyOptional({ type: [String], description: 'The domains of the account', }) + @IsOptional() domains?: string[]; @ApiPropertyOptional({ @@ -15,22 +18,29 @@ export class UnifiedAccountInput { description: 'The custom field mappings of the account between the remote 3rd party & Panora', }) + @IsOptional() field_mappings?: Record; } export class UnifiedAccountOutput extends UnifiedAccountInput { - @ApiPropertyOptional({ description: 'The uuid of the account' }) + @ApiPropertyOptional({ type: String, description: 'The uuid of the account' }) + @IsUUID() + @IsOptional() id?: string; @ApiPropertyOptional({ + type: String, description: 'The id of the account in the context of the 3rd Party', }) + @IsString() + @IsOptional() remote_id?: string; @ApiPropertyOptional({ - type: [{}], + type: {}, description: 'The remote data of the account in the context of the 3rd Party', }) + @IsOptional() remote_data?: Record; } diff --git a/packages/api/src/ticketing/attachment/attachment.module.ts b/packages/api/src/ticketing/attachment/attachment.module.ts index 935a8bb6a..de94b589d 100644 --- a/packages/api/src/ticketing/attachment/attachment.module.ts +++ b/packages/api/src/ticketing/attachment/attachment.module.ts @@ -11,9 +11,12 @@ import { BullModule } from '@nestjs/bull'; @Module({ imports: [ - BullModule.registerQueue({ - name: 'webhookDelivery', - }), + BullModule.registerQueue( + { + name: 'webhookDelivery', + }, + { name: 'syncTasks' }, + ), ], controllers: [AttachmentController], providers: [ diff --git a/packages/api/src/ticketing/attachment/types/model.unified.ts b/packages/api/src/ticketing/attachment/types/model.unified.ts index 354150082..710bbed29 100644 --- a/packages/api/src/ticketing/attachment/types/model.unified.ts +++ b/packages/api/src/ticketing/attachment/types/model.unified.ts @@ -1,13 +1,21 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsOptional, IsString, IsUUID } from 'class-validator'; export class UnifiedAttachmentInput { - @ApiProperty({ description: 'The file name of the attachment' }) + @ApiProperty({ type: String, description: 'The file name of the attachment' }) + @IsString() file_name: string; - @ApiProperty({ description: 'The file url of the attachment' }) + @ApiProperty({ type: String, description: 'The file url of the attachment' }) + @IsString() file_url: string; - @ApiProperty({ description: "The uploader's uuid of the attachment" }) + @ApiProperty({ + type: String, + description: "The uploader's uuid of the attachment", + }) + @IsString() + @IsOptional() uploader?: string; @ApiPropertyOptional({ @@ -15,22 +23,32 @@ export class UnifiedAttachmentInput { description: 'The custom field mappings of the attachment between the remote 3rd party & Panora', }) + @IsOptional() field_mappings?: Record; } export class UnifiedAttachmentOutput extends UnifiedAttachmentInput { - @ApiPropertyOptional({ description: 'The uuid of the attachment' }) + @ApiPropertyOptional({ + type: String, + description: 'The uuid of the attachment', + }) + @IsUUID() + @IsOptional() id?: string; @ApiPropertyOptional({ + type: String, description: 'The id of the attachment in the context of the 3rd Party', }) + @IsString() + @IsOptional() remote_id?: string; @ApiPropertyOptional({ - type: [{}], + type: {}, description: 'The remote data of the attachment in the context of the 3rd Party', }) + @IsOptional() remote_data?: Record; } diff --git a/packages/api/src/ticketing/collection/collection.module.ts b/packages/api/src/ticketing/collection/collection.module.ts index 1be92ac99..03dd577e7 100644 --- a/packages/api/src/ticketing/collection/collection.module.ts +++ b/packages/api/src/ticketing/collection/collection.module.ts @@ -13,9 +13,12 @@ import { JiraService } from './services/jira'; @Module({ imports: [ - BullModule.registerQueue({ - name: 'webhookDelivery', - }), + BullModule.registerQueue( + { + name: 'webhookDelivery', + }, + { name: 'syncTasks' }, + ), ], controllers: [CollectionController], providers: [ @@ -30,6 +33,13 @@ import { JiraService } from './services/jira'; /* PROVIDERS SERVICES */ JiraService, ], - exports: [SyncService], + exports: [ + SyncService, + ServiceRegistry, + WebhookService, + FieldMappingService, + LoggerService, + PrismaService, + ], }) export class CollectionModule {} diff --git a/packages/api/src/ticketing/collection/sync/sync.processor.ts b/packages/api/src/ticketing/collection/sync/sync.processor.ts new file mode 100644 index 000000000..a2304ea9d --- /dev/null +++ b/packages/api/src/ticketing/collection/sync/sync.processor.ts @@ -0,0 +1,17 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; + +@Processor('syncTasks') +export class SyncProcessor { + constructor(private syncService: SyncService) {} + @Process('ticketing-sync-collections') + async handleSyncCollections(job: Job) { + try { + console.log(`Processing queue -> ticketing-sync-collections ${job.id}`); + await this.syncService.syncCollections(); + } catch (error) { + console.error('Error syncing ticketing collections', error); + } + } +} diff --git a/packages/api/src/ticketing/collection/sync/sync.service.ts b/packages/api/src/ticketing/collection/sync/sync.service.ts index b8c4af4f8..fca3711db 100644 --- a/packages/api/src/ticketing/collection/sync/sync.service.ts +++ b/packages/api/src/ticketing/collection/sync/sync.service.ts @@ -15,6 +15,8 @@ import { ICollectionService } from '../types'; import { OriginalCollectionOutput } from '@@core/utils/types/original/original.ticketing'; import { tcg_collections as TicketingCollection } from '@prisma/client'; import { TICKETING_PROVIDERS } from '@panora/shared'; +import { InjectQueue } from '@nestjs/bull'; +import { Queue } from 'bull'; @Injectable() export class SyncService implements OnModuleInit { @@ -22,27 +24,55 @@ export class SyncService implements OnModuleInit { private prisma: PrismaService, private logger: LoggerService, private webhook: WebhookService, - private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + @InjectQueue('syncTasks') private syncQueue: Queue, ) { this.logger.setContext(SyncService.name); } async onModuleInit() { try { - await this.syncCollections(); + await this.scheduleSyncJob(); } catch (error) { handleServiceError(error, this.logger); } } - @Cron('*/20 * * * *') + private async scheduleSyncJob() { + const jobName = 'ticketing-sync-collections'; + + // Remove existing jobs to avoid duplicates in case of application restart + const jobs = await this.syncQueue.getRepeatableJobs(); + for (const job of jobs) { + if (job.name === jobName) { + await this.syncQueue.removeRepeatableByKey(job.key); + } + } + // Add new job to the queue with a CRON expression + await this.syncQueue.add( + jobName, + {}, + { + repeat: { cron: '0 0 * * *' }, // Runs once a day at midnight + }, + ); + } //function used by sync worker which populate our tcg_collections table //its role is to fetch all collections from providers 3rd parties and save the info inside our db - async syncCollections() { + //@Cron('*/2 * * * *') // every 2 minutes (for testing) + @Cron('0 */8 * * *') // every 8 hours + async syncCollections(user_id?: string) { try { this.logger.log(`Syncing collections....`); - const users = await this.prisma.users.findMany(); + const users = user_id + ? [ + await this.prisma.users.findUnique({ + where: { + id_user: user_id, + }, + }), + ] + : await this.prisma.users.findMany(); if (users && users.length > 0) { for (const user of users) { const projects = await this.prisma.projects.findMany({ diff --git a/packages/api/src/ticketing/collection/types/model.unified.ts b/packages/api/src/ticketing/collection/types/model.unified.ts index f6fe9af06..4329514c6 100644 --- a/packages/api/src/ticketing/collection/types/model.unified.ts +++ b/packages/api/src/ticketing/collection/types/model.unified.ts @@ -1,35 +1,56 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsIn, IsOptional, IsString, IsUUID } from 'class-validator'; export class UnifiedCollectionInput { @ApiProperty({ + type: String, description: 'The name of the collection', }) + @IsString() name: string; @ApiPropertyOptional({ + type: String, description: 'The description of the collection', }) + @IsString() + @IsOptional() description?: string; @ApiPropertyOptional({ - description: 'The type of the collection, either PROJECT or LIST ', + type: String, + description: + 'The type of the collection. Authorized values are either PROJECT or LIST ', + }) + @IsIn(['PROJECT', 'LIST'], { + message: 'Type must be either PROJECT or LIST', }) + @IsOptional() collection_type?: string; } export class UnifiedCollectionOutput extends UnifiedCollectionInput { - @ApiPropertyOptional({ description: 'The uuid of the collection' }) + @ApiPropertyOptional({ + type: String, + description: 'The uuid of the collection', + }) + @IsUUID() + @IsOptional() id?: string; @ApiPropertyOptional({ + type: String, description: 'The id of the collection in the context of the 3rd Party', }) + @IsString() + @IsOptional() remote_id?: string; @ApiPropertyOptional({ - type: [{}], + type: {}, description: 'The remote data of the collection in the context of the 3rd Party', }) + @IsOptional() remote_data?: Record; } diff --git a/packages/api/src/ticketing/comment/comment.module.ts b/packages/api/src/ticketing/comment/comment.module.ts index 0747d1023..42df81c12 100644 --- a/packages/api/src/ticketing/comment/comment.module.ts +++ b/packages/api/src/ticketing/comment/comment.module.ts @@ -16,9 +16,12 @@ import { GorgiasService } from './services/gorgias'; @Module({ imports: [ - BullModule.registerQueue({ - name: 'webhookDelivery', - }), + BullModule.registerQueue( + { + name: 'webhookDelivery', + }, + { name: 'syncTasks' }, + ), ], controllers: [CommentController], providers: [ @@ -36,6 +39,13 @@ import { GorgiasService } from './services/gorgias'; JiraService, GorgiasService, ], - exports: [SyncService], + exports: [ + SyncService, + ServiceRegistry, + WebhookService, + FieldMappingService, + LoggerService, + PrismaService, + ], }) export class CommentModule {} diff --git a/packages/api/src/ticketing/comment/services/front/mappers.ts b/packages/api/src/ticketing/comment/services/front/mappers.ts index 1e28303b8..b48a9b593 100644 --- a/packages/api/src/ticketing/comment/services/front/mappers.ts +++ b/packages/api/src/ticketing/comment/services/front/mappers.ts @@ -80,7 +80,7 @@ export class FrontCommentMapper implements ICommentMapper { if (user_id) { // we must always fall here for Front - opts = { user_id: user_id, creator_type: 'user' }; + opts = { user_id: user_id, creator_type: 'USER' }; } } diff --git a/packages/api/src/ticketing/comment/services/gorgias/mappers.ts b/packages/api/src/ticketing/comment/services/gorgias/mappers.ts index 3b3d5b74f..cd41e92dd 100644 --- a/packages/api/src/ticketing/comment/services/gorgias/mappers.ts +++ b/packages/api/src/ticketing/comment/services/gorgias/mappers.ts @@ -96,7 +96,7 @@ export class GorgiasCommentMapper implements ICommentMapper { 'gorgias', ); if (contact_id) { - opts = { creator_type: 'contact', contact_id: contact_id }; + opts = { creator_type: 'CONTACT', contact_id: contact_id }; } } } diff --git a/packages/api/src/ticketing/comment/services/jira/mappers.ts b/packages/api/src/ticketing/comment/services/jira/mappers.ts index 7e16c588a..c25798828 100644 --- a/packages/api/src/ticketing/comment/services/jira/mappers.ts +++ b/packages/api/src/ticketing/comment/services/jira/mappers.ts @@ -73,7 +73,7 @@ export class JiraCommentMapper implements ICommentMapper { if (user_id) { // we must always fall here for Jira - opts = { user_id: user_id, creator_type: 'user' }; + opts = { user_id: user_id, creator_type: 'USER' }; } } diff --git a/packages/api/src/ticketing/comment/services/zendesk/mappers.ts b/packages/api/src/ticketing/comment/services/zendesk/mappers.ts index 25e8553ea..ea6e567e7 100644 --- a/packages/api/src/ticketing/comment/services/zendesk/mappers.ts +++ b/packages/api/src/ticketing/comment/services/zendesk/mappers.ts @@ -30,10 +30,15 @@ export class ZendeskCommentMapper implements ICommentMapper { type: 'Comment', }; - if (source.contact_id) { - result.author_id = source.contact_id - ? Number(await this.utils.getContactRemoteIdFromUuid(source.contact_id)) - : Number(await this.utils.getUserRemoteIdFromUuid(source.user_id)); + if (source.creator_type === 'USER') { + result.author_id = Number( + await this.utils.getUserRemoteIdFromUuid(source.user_id), + ); + } + if (source.creator_type === 'CONTACT') { + result.author_id = Number( + await this.utils.getContactRemoteIdFromUuid(source.contact_id), + ); } if (source.attachments) { @@ -92,14 +97,14 @@ export class ZendeskCommentMapper implements ICommentMapper { ); if (user_id) { - opts = { user_id: user_id, creator_type: 'user' }; + opts = { user_id: user_id, creator_type: 'USER' }; } else { const contact_id = await this.utils.getContactUuidFromRemoteId( String(comment.author_id), 'zendesk', ); if (contact_id) { - opts = { creator_type: 'contact', contact_id: contact_id }; + opts = { creator_type: 'CONTACT', contact_id: contact_id }; } } } diff --git a/packages/api/src/ticketing/comment/sync/sync.processor.ts b/packages/api/src/ticketing/comment/sync/sync.processor.ts new file mode 100644 index 000000000..ea8ee9b5b --- /dev/null +++ b/packages/api/src/ticketing/comment/sync/sync.processor.ts @@ -0,0 +1,17 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; + +@Processor('syncTasks') +export class SyncProcessor { + constructor(private syncService: SyncService) {} + @Process('ticketing-sync-comments') + async handleSyncComments(job: Job) { + try { + console.log(`Processing queue -> ticketing-sync-comments ${job.id}`); + await this.syncService.syncComments(); + } catch (error) { + console.error('Error syncing ticketing comments', error); + } + } +} diff --git a/packages/api/src/ticketing/comment/sync/sync.service.ts b/packages/api/src/ticketing/comment/sync/sync.service.ts index 0b80c8d67..f6147b1e9 100644 --- a/packages/api/src/ticketing/comment/sync/sync.service.ts +++ b/packages/api/src/ticketing/comment/sync/sync.service.ts @@ -15,6 +15,8 @@ import { ICommentService } from '../types'; import { OriginalCommentOutput } from '@@core/utils/types/original/original.ticketing'; import { ServiceRegistry } from '../services/registry.service'; import { TICKETING_PROVIDERS } from '@panora/shared'; +import { InjectQueue } from '@nestjs/bull'; +import { Queue } from 'bull'; @Injectable() export class SyncService implements OnModuleInit { @@ -24,25 +26,54 @@ export class SyncService implements OnModuleInit { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + @InjectQueue('syncTasks') private syncQueue: Queue, ) { this.logger.setContext(SyncService.name); } async onModuleInit() { try { - await this.syncComments(); + await this.scheduleSyncJob(); } catch (error) { handleServiceError(error, this.logger); } } - @Cron('*/20 * * * *') + private async scheduleSyncJob() { + const jobName = 'ticketing-sync-comments'; + + // Remove existing jobs to avoid duplicates in case of application restart + const jobs = await this.syncQueue.getRepeatableJobs(); + for (const job of jobs) { + if (job.name === jobName) { + await this.syncQueue.removeRepeatableByKey(job.key); + } + } + // Add new job to the queue with a CRON expression + await this.syncQueue.add( + jobName, + {}, + { + repeat: { cron: '0 0 * * *' }, // Runs once a day at midnight + }, + ); + } //function used by sync worker which populate our tcg_comments table //its role is to fetch all comments from providers 3rd parties and save the info inside our db - async syncComments() { + //@Cron('*/2 * * * *') // every 2 minutes (for testing) + @Cron('0 */8 * * *') // every 8 hours + async syncComments(user_id?: string) { try { this.logger.log(`Syncing comments....`); - const users = await this.prisma.users.findMany(); + const users = user_id + ? [ + await this.prisma.users.findUnique({ + where: { + id_user: user_id, + }, + }), + ] + : await this.prisma.users.findMany(); if (users && users.length > 0) { for (const user of users) { const projects = await this.prisma.projects.findMany({ diff --git a/packages/api/src/ticketing/comment/types/model.unified.ts b/packages/api/src/ticketing/comment/types/model.unified.ts index 56030c0ca..5f5e4a3f3 100644 --- a/packages/api/src/ticketing/comment/types/model.unified.ts +++ b/packages/api/src/ticketing/comment/types/model.unified.ts @@ -1,67 +1,99 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { UnifiedAttachmentOutput } from '@ticketing/attachment/types/model.unified'; +import { IsBoolean, IsIn, IsOptional, IsString, IsUUID } from 'class-validator'; export class UnifiedCommentInput { - @ApiProperty({ description: 'The body of the comment' }) + @ApiProperty({ type: String, description: 'The body of the comment' }) + @IsString() body: string; - @ApiPropertyOptional({ description: 'The html body of the comment' }) + @ApiPropertyOptional({ + type: String, + description: 'The html body of the comment', + }) + @IsString() + @IsOptional() html_body?: string; @ApiPropertyOptional({ type: Boolean, description: 'The public status of the comment', }) + @IsOptional() + @IsBoolean() is_private?: boolean; @ApiPropertyOptional({ - description: 'The creator type of the comment (either user or contact)', + type: String, + description: + 'The creator type of the comment. Authorized values are either USER or CONTACT', + }) + @IsIn(['USER', 'CONTACT'], { + message: 'Type must be either USER or CONTACT', }) - creator_type?: 'user' | 'contact'; + @IsOptional() + creator_type?: string; @ApiPropertyOptional({ + type: String, description: 'The uuid of the ticket the comment is tied to', }) + @IsUUID() + @IsOptional() ticket_id?: string; // uuid of Ticket object @ApiPropertyOptional({ + type: String, description: 'The uuid of the contact which the comment belongs to (if no user_id specified)', }) + @IsUUID() + @IsOptional() contact_id?: string; // uuid of Contact object @ApiPropertyOptional({ + type: String, description: 'The uuid of the user which the comment belongs to (if no contact_id specified)', }) + @IsUUID() + @IsOptional() user_id?: string; // uuid of User object @ApiPropertyOptional({ type: [String], description: 'The attachements uuids tied to the comment', }) + @IsOptional() attachments?: any[]; //uuids of Attachments objects } export class UnifiedCommentOutput extends UnifiedCommentInput { - @ApiPropertyOptional({ description: 'The uuid of the comment' }) + @ApiPropertyOptional({ type: String, description: 'The uuid of the comment' }) + @IsUUID() + @IsOptional() id?: string; @ApiPropertyOptional({ + type: String, description: 'The id of the comment in the context of the 3rd Party', }) + @IsString() + @IsOptional() remote_id?: string; @ApiPropertyOptional({ - type: [{}], + type: {}, description: 'The remote data of the comment in the context of the 3rd Party', }) + @IsOptional() remote_data?: Record; @ApiPropertyOptional({ type: [UnifiedAttachmentOutput], description: 'The attachemnets tied to the comment', }) + @IsOptional() attachments?: UnifiedAttachmentOutput[]; // Attachments objects } diff --git a/packages/api/src/ticketing/contact/contact.module.ts b/packages/api/src/ticketing/contact/contact.module.ts index ad2e375cd..b0858a62f 100644 --- a/packages/api/src/ticketing/contact/contact.module.ts +++ b/packages/api/src/ticketing/contact/contact.module.ts @@ -15,9 +15,12 @@ import { GorgiasService } from './services/gorgias'; @Module({ imports: [ - BullModule.registerQueue({ - name: 'webhookDelivery', - }), + BullModule.registerQueue( + { + name: 'webhookDelivery', + }, + { name: 'syncTasks' }, + ), ], controllers: [ContactController], providers: [ @@ -34,6 +37,13 @@ import { GorgiasService } from './services/gorgias'; FrontService, GorgiasService, ], - exports: [SyncService], + exports: [ + SyncService, + ServiceRegistry, + WebhookService, + FieldMappingService, + LoggerService, + PrismaService, + ], }) export class ContactModule {} diff --git a/packages/api/src/ticketing/contact/sync/sync.processor.ts b/packages/api/src/ticketing/contact/sync/sync.processor.ts new file mode 100644 index 000000000..e810ef014 --- /dev/null +++ b/packages/api/src/ticketing/contact/sync/sync.processor.ts @@ -0,0 +1,17 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; + +@Processor('syncTasks') +export class SyncProcessor { + constructor(private syncService: SyncService) {} + @Process('ticketing-sync-contacts') + async handleSyncContacts(job: Job) { + try { + console.log(`Processing queue -> ticketing-sync-contacts ${job.id}`); + await this.syncService.syncContacts(); + } catch (error) { + console.error('Error syncing ticketing contacts', error); + } + } +} diff --git a/packages/api/src/ticketing/contact/sync/sync.service.ts b/packages/api/src/ticketing/contact/sync/sync.service.ts index 7f0ae33ec..1999c7803 100644 --- a/packages/api/src/ticketing/contact/sync/sync.service.ts +++ b/packages/api/src/ticketing/contact/sync/sync.service.ts @@ -15,6 +15,8 @@ import { ServiceRegistry } from '../services/registry.service'; import { tcg_contacts as TicketingContact } from '@prisma/client'; import { OriginalContactOutput } from '@@core/utils/types/original/original.ticketing'; import { TICKETING_PROVIDERS } from '@panora/shared'; +import { InjectQueue } from '@nestjs/bull'; +import { Queue } from 'bull'; @Injectable() export class SyncService implements OnModuleInit { @@ -24,25 +26,54 @@ export class SyncService implements OnModuleInit { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + @InjectQueue('syncTasks') private syncQueue: Queue, ) { this.logger.setContext(SyncService.name); } async onModuleInit() { try { - await this.syncContacts(); + await this.scheduleSyncJob(); } catch (error) { handleServiceError(error, this.logger); } } - @Cron('*/20 * * * *') + private async scheduleSyncJob() { + const jobName = 'ticketing-sync-contacts'; + + // Remove existing jobs to avoid duplicates in case of application restart + const jobs = await this.syncQueue.getRepeatableJobs(); + for (const job of jobs) { + if (job.name === jobName) { + await this.syncQueue.removeRepeatableByKey(job.key); + } + } + // Add new job to the queue with a CRON expression + await this.syncQueue.add( + jobName, + {}, + { + repeat: { cron: '0 0 * * *' }, // Runs once a day at midnight + }, + ); + } //function used by sync worker which populate our tcg_contacts table //its role is to fetch all contacts from providers 3rd parties and save the info inside our db - async syncContacts() { + //@Cron('*/2 * * * *') // every 2 minutes (for testing) + @Cron('0 */8 * * *') // every 8 hours + async syncContacts(user_id?: string) { try { this.logger.log(`Syncing contacts....`); - const users = await this.prisma.users.findMany(); + const users = user_id + ? [ + await this.prisma.users.findUnique({ + where: { + id_user: user_id, + }, + }), + ] + : await this.prisma.users.findMany(); if (users && users.length > 0) { for (const user of users) { const projects = await this.prisma.projects.findMany({ diff --git a/packages/api/src/ticketing/contact/types/model.unified.ts b/packages/api/src/ticketing/contact/types/model.unified.ts index a404c6157..90fdc5c34 100644 --- a/packages/api/src/ticketing/contact/types/model.unified.ts +++ b/packages/api/src/ticketing/contact/types/model.unified.ts @@ -1,24 +1,35 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsOptional, IsString, IsUUID } from 'class-validator'; export class UnifiedContactInput { @ApiProperty({ + type: String, description: 'The name of the contact', }) + @IsString() name: string; @ApiProperty({ + type: String, description: 'The email address of the contact', }) + @IsString() email_address: string; @ApiPropertyOptional({ + type: String, description: 'The phone number of the contact', }) + @IsString() + @IsOptional() phone_number?: string; @ApiPropertyOptional({ + type: String, description: 'The details of the contact', }) + @IsOptional() + @IsString() details?: string; @ApiPropertyOptional({ @@ -26,22 +37,29 @@ export class UnifiedContactInput { description: 'The custom field mappings of the contact between the remote 3rd party & Panora', }) + @IsOptional() field_mappings?: Record; } export class UnifiedContactOutput extends UnifiedContactInput { - @ApiPropertyOptional({ description: 'The uuid of the contact' }) + @ApiPropertyOptional({ type: String, description: 'The uuid of the contact' }) + @IsUUID() + @IsOptional() id?: string; @ApiPropertyOptional({ + type: String, description: 'The id of the contact in the context of the 3rd Party', }) + @IsOptional() + @IsString() remote_id?: string; @ApiPropertyOptional({ - type: [{}], + type: {}, description: 'The remote data of the contact in the context of the 3rd Party', }) + @IsOptional() remote_data?: Record; } diff --git a/packages/api/src/ticketing/tag/sync/sync.processor.ts b/packages/api/src/ticketing/tag/sync/sync.processor.ts new file mode 100644 index 000000000..47ecea372 --- /dev/null +++ b/packages/api/src/ticketing/tag/sync/sync.processor.ts @@ -0,0 +1,17 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; + +@Processor('syncTasks') +export class SyncProcessor { + constructor(private syncService: SyncService) {} + @Process('ticketing-sync-tags') + async handleSyncTags(job: Job) { + try { + console.log(`Processing queue -> ticketing-sync-tags ${job.id}`); + await this.syncService.syncTags(); + } catch (error) { + console.error('Error syncing ticketing tags', error); + } + } +} diff --git a/packages/api/src/ticketing/tag/sync/sync.service.ts b/packages/api/src/ticketing/tag/sync/sync.service.ts index 980ede2fc..13b1d250e 100644 --- a/packages/api/src/ticketing/tag/sync/sync.service.ts +++ b/packages/api/src/ticketing/tag/sync/sync.service.ts @@ -15,6 +15,8 @@ import { ITagService } from '../types'; import { OriginalTagOutput } from '@@core/utils/types/original/original.ticketing'; import { tcg_tags as TicketingTag } from '@prisma/client'; import { TICKETING_PROVIDERS } from '@panora/shared'; +import { InjectQueue } from '@nestjs/bull'; +import { Queue } from 'bull'; @Injectable() export class SyncService implements OnModuleInit { @@ -24,25 +26,54 @@ export class SyncService implements OnModuleInit { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + @InjectQueue('syncTasks') private syncQueue: Queue, ) { this.logger.setContext(SyncService.name); } async onModuleInit() { try { - await this.syncTags(); + await this.scheduleSyncJob(); } catch (error) { handleServiceError(error, this.logger); } } - @Cron('*/20 * * * *') + private async scheduleSyncJob() { + const jobName = 'ticketing-sync-tags'; + + // Remove existing jobs to avoid duplicates in case of application restart + const jobs = await this.syncQueue.getRepeatableJobs(); + for (const job of jobs) { + if (job.name === jobName) { + await this.syncQueue.removeRepeatableByKey(job.key); + } + } + // Add new job to the queue with a CRON expression + await this.syncQueue.add( + jobName, + {}, + { + repeat: { cron: '0 0 * * *' }, // Runs once a day at midnight + }, + ); + } //function used by sync worker which populate our tcg_tags table //its role is to fetch all tags from providers 3rd parties and save the info inside our db - async syncTags() { + //@Cron('*/2 * * * *') // every 2 minutes (for testing) + @Cron('0 */8 * * *') // every 8 hours + async syncTags(user_id?: string) { try { this.logger.log(`Syncing tags....`); - const users = await this.prisma.users.findMany(); + const users = user_id + ? [ + await this.prisma.users.findUnique({ + where: { + id_user: user_id, + }, + }), + ] + : await this.prisma.users.findMany(); if (users && users.length > 0) { for (const user of users) { const projects = await this.prisma.projects.findMany({ diff --git a/packages/api/src/ticketing/tag/tag.module.ts b/packages/api/src/ticketing/tag/tag.module.ts index 9e2cb2904..1e2983fab 100644 --- a/packages/api/src/ticketing/tag/tag.module.ts +++ b/packages/api/src/ticketing/tag/tag.module.ts @@ -16,9 +16,12 @@ import { GorgiasService } from './services/gorgias'; @Module({ imports: [ - BullModule.registerQueue({ - name: 'webhookDelivery', - }), + BullModule.registerQueue( + { + name: 'webhookDelivery', + }, + { name: 'syncTasks' }, + ), ], controllers: [TagController], providers: [ @@ -36,6 +39,13 @@ import { GorgiasService } from './services/gorgias'; JiraService, GorgiasService, ], - exports: [SyncService], + exports: [ + SyncService, + ServiceRegistry, + WebhookService, + FieldMappingService, + LoggerService, + PrismaService, + ], }) export class TagModule {} diff --git a/packages/api/src/ticketing/tag/types/model.unified.ts b/packages/api/src/ticketing/tag/types/model.unified.ts index 5380a2e32..3693535c3 100644 --- a/packages/api/src/ticketing/tag/types/model.unified.ts +++ b/packages/api/src/ticketing/tag/types/model.unified.ts @@ -1,9 +1,12 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsOptional, IsString, IsUUID } from 'class-validator'; export class UnifiedTagInput { @ApiProperty({ + type: String, description: 'The name of the tag', }) + @IsString() name: string; @ApiPropertyOptional({ @@ -11,21 +14,28 @@ export class UnifiedTagInput { description: 'The custom field mappings of the tag between the remote 3rd party & Panora', }) + @IsOptional() field_mappings?: Record; } export class UnifiedTagOutput extends UnifiedTagInput { - @ApiPropertyOptional({ description: 'The uuid of the tag' }) + @ApiPropertyOptional({ type: String, description: 'The uuid of the tag' }) + @IsUUID() + @IsOptional() id?: string; @ApiPropertyOptional({ + type: String, description: 'The id of the tag in the context of the 3rd Party', }) + @IsString() + @IsOptional() remote_id?: string; @ApiPropertyOptional({ - type: [{}], + type: {}, description: 'The remote data of the tag in the context of the 3rd Party', }) + @IsOptional() remote_data?: Record; } diff --git a/packages/api/src/ticketing/team/sync/sync.processor.ts b/packages/api/src/ticketing/team/sync/sync.processor.ts new file mode 100644 index 000000000..c31a7bbc0 --- /dev/null +++ b/packages/api/src/ticketing/team/sync/sync.processor.ts @@ -0,0 +1,17 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; + +@Processor('syncTasks') +export class SyncProcessor { + constructor(private syncService: SyncService) {} + @Process('ticketing-sync-teams') + async handleSyncTeams(job: Job) { + try { + console.log(`Processing queue -> ticketing-sync-teams ${job.id}`); + await this.syncService.syncTeams(); + } catch (error) { + console.error('Error syncing ticketing teams', error); + } + } +} diff --git a/packages/api/src/ticketing/team/sync/sync.service.ts b/packages/api/src/ticketing/team/sync/sync.service.ts index 32107f61a..e18098e67 100644 --- a/packages/api/src/ticketing/team/sync/sync.service.ts +++ b/packages/api/src/ticketing/team/sync/sync.service.ts @@ -15,6 +15,8 @@ import { ITeamService } from '../types'; import { tcg_teams as TicketingTeam } from '@prisma/client'; import { OriginalTeamOutput } from '@@core/utils/types/original/original.ticketing'; import { TICKETING_PROVIDERS } from '@panora/shared'; +import { InjectQueue } from '@nestjs/bull'; +import { Queue } from 'bull'; @Injectable() export class SyncService implements OnModuleInit { @@ -24,25 +26,54 @@ export class SyncService implements OnModuleInit { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + @InjectQueue('syncTasks') private syncQueue: Queue, ) { this.logger.setContext(SyncService.name); } async onModuleInit() { try { - await this.syncTeams(); + await this.scheduleSyncJob(); } catch (error) { handleServiceError(error, this.logger); } } - @Cron('*/20 * * * *') + private async scheduleSyncJob() { + const jobName = 'ticketing-sync-teams'; + + // Remove existing jobs to avoid duplicates in case of application restart + const jobs = await this.syncQueue.getRepeatableJobs(); + for (const job of jobs) { + if (job.name === jobName) { + await this.syncQueue.removeRepeatableByKey(job.key); + } + } + // Add new job to the queue with a CRON expression + await this.syncQueue.add( + jobName, + {}, + { + repeat: { cron: '0 0 * * *' }, // Runs once a day at midnight + }, + ); + } //function used by sync worker which populate our tcg_teams table //its role is to fetch all teams from providers 3rd parties and save the info inside our db - async syncTeams() { + //@Cron('*/2 * * * *') // every 2 minutes (for testing) + @Cron('0 */8 * * *') // every 8 hours + async syncTeams(user_id?: string) { try { this.logger.log(`Syncing teams....`); - const users = await this.prisma.users.findMany(); + const users = user_id + ? [ + await this.prisma.users.findUnique({ + where: { + id_user: user_id, + }, + }), + ] + : await this.prisma.users.findMany(); if (users && users.length > 0) { for (const user of users) { const projects = await this.prisma.projects.findMany({ diff --git a/packages/api/src/ticketing/team/team.module.ts b/packages/api/src/ticketing/team/team.module.ts index aba6d0e58..3b206c257 100644 --- a/packages/api/src/ticketing/team/team.module.ts +++ b/packages/api/src/ticketing/team/team.module.ts @@ -16,9 +16,12 @@ import { GorgiasService } from './services/gorgias'; @Module({ imports: [ - BullModule.registerQueue({ - name: 'webhookDelivery', - }), + BullModule.registerQueue( + { + name: 'webhookDelivery', + }, + { name: 'syncTasks' }, + ), ], controllers: [TeamController], providers: [ @@ -36,6 +39,13 @@ import { GorgiasService } from './services/gorgias'; JiraService, GorgiasService, ], - exports: [SyncService], + exports: [ + SyncService, + ServiceRegistry, + WebhookService, + FieldMappingService, + LoggerService, + PrismaService, + ], }) export class TeamModule {} diff --git a/packages/api/src/ticketing/team/types/model.unified.ts b/packages/api/src/ticketing/team/types/model.unified.ts index ce85bfff5..b6b9e4013 100644 --- a/packages/api/src/ticketing/team/types/model.unified.ts +++ b/packages/api/src/ticketing/team/types/model.unified.ts @@ -1,14 +1,20 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsOptional, IsString, IsUUID } from 'class-validator'; export class UnifiedTeamInput { @ApiProperty({ + type: String, description: 'The name of the team', }) + @IsString() name: string; @ApiPropertyOptional({ + type: String, description: 'The description of the team', }) + @IsString() + @IsOptional() description?: string; @ApiPropertyOptional({ @@ -16,21 +22,28 @@ export class UnifiedTeamInput { description: 'The custom field mappings of the team between the remote 3rd party & Panora', }) + @IsOptional() field_mappings?: Record; } export class UnifiedTeamOutput extends UnifiedTeamInput { - @ApiPropertyOptional({ description: 'The uuid of the team' }) + @ApiPropertyOptional({ type: String, description: 'The uuid of the team' }) + @IsUUID() + @IsOptional() id?: string; @ApiPropertyOptional({ + type: String, description: 'The id of the team in the context of the 3rd Party', }) + @IsString() + @IsOptional() remote_id?: string; @ApiPropertyOptional({ - type: [{}], + type: {}, description: 'The remote data of the team in the context of the 3rd Party', }) + @IsOptional() remote_data?: Record; } diff --git a/packages/api/src/ticketing/ticket/services/gorgias/mappers.ts b/packages/api/src/ticketing/ticket/services/gorgias/mappers.ts index 832efae98..1abf8dd1b 100644 --- a/packages/api/src/ticketing/ticket/services/gorgias/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/gorgias/mappers.ts @@ -52,7 +52,7 @@ export class GorgiasTicketMapper implements ITicketMapper { }; if (source.status) { - result.status = source.status; + result.status = source.status.toLowerCase(); } if (source.assigned_to && source.assigned_to.length > 0) { diff --git a/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts b/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts index 008ab0128..4958c6f7a 100644 --- a/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/hubspot/mappers.ts @@ -15,9 +15,9 @@ export class HubspotTicketMapper implements ITicketMapper { ): HubspotTicketInput { const result = { subject: source.name, - hs_pipeline: source.type || '', - hubspot_owner_id: '', // TODO Replace 'default' with actual owner ID - hs_pipeline_stage: source.status || '', + hs_pipeline: '', + hubspot_owner_id: '', + hs_pipeline_stage: '', hs_ticket_priority: source.priority || 'MEDIUM', }; @@ -65,11 +65,11 @@ export class HubspotTicketMapper implements ITicketMapper { } return { - name: ticket.properties.name, //TODO - status: ticket.properties.hs_pipeline_stage, - description: ticket.properties.description, //TODO + name: ticket.properties.name, + status: '', // hs_pipeline_stage: '', + description: ticket.properties.description, due_date: new Date(ticket.properties.createdate), - type: ticket.properties.hs_pipeline, + type: '', //ticket.properties.hs_pipeline, parent_ticket: '', // Define how you determine the parent ticket completed_at: new Date(ticket.properties.hs_lastmodifieddate), priority: ticket.properties.hs_ticket_priority, diff --git a/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts b/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts index b1fc033c2..0d9e4f4f4 100644 --- a/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts +++ b/packages/api/src/ticketing/ticket/services/zendesk/mappers.ts @@ -41,22 +41,22 @@ export class ZendeskTicketMapper implements ITicketMapper { result.due_at = source.due_date?.toISOString(); } if (source.priority) { - result.priority = source.priority as 'urgent' | 'high' | 'normal' | 'low'; + result.priority = ( + source.priority == 'MEDIUM' ? 'normal' : source.priority.toLowerCase() + ) as 'urgent' | 'high' | 'normal' | 'low'; } if (source.status) { - result.status = source.status as - | 'new' - | 'open' - | 'pending' - | 'hold' - | 'solved' - | 'closed'; + result.status = source.status.toLowerCase() as 'open' | 'closed'; } if (source.tags) { result.tags = source.tags; } if (source.type) { - result.type = source.type as 'problem' | 'incident' | 'question' | 'task'; + result.type = source.type.toLowerCase() as + | 'problem' + | 'incident' + | 'question' + | 'task'; } if (customFieldMappings && source.field_mappings) { @@ -128,10 +128,11 @@ export class ZendeskTicketMapper implements ITicketMapper { const unifiedTicket: UnifiedTicketOutput = { name: ticket.subject, - status: ticket.status, + status: + ticket.status === 'new' || ticket.status === 'open' ? 'OPEN' : 'CLOSED', // todo: handle pending status ? description: ticket.description, due_date: ticket.due_at ? new Date(ticket.due_at) : undefined, - type: ticket.type, + type: ticket.type === 'incident' ? 'PROBLEM' : ticket.type.toUpperCase(), parent_ticket: undefined, // If available, add logic to map parent ticket tags: ticket.tags, completed_at: new Date(ticket.updated_at), diff --git a/packages/api/src/ticketing/ticket/sync/sync.processor.ts b/packages/api/src/ticketing/ticket/sync/sync.processor.ts new file mode 100644 index 000000000..ba071968d --- /dev/null +++ b/packages/api/src/ticketing/ticket/sync/sync.processor.ts @@ -0,0 +1,17 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; + +@Processor('syncTasks') +export class SyncProcessor { + constructor(private syncService: SyncService) {} + @Process('ticketing-sync-tickets') + async handleSyncTickets(job: Job) { + try { + console.log(`Processing queue -> ticketing-sync-tickets ${job.id}`); + await this.syncService.syncTickets(); + } catch (error) { + console.error('Error syncing ticketing tickets', error); + } + } +} diff --git a/packages/api/src/ticketing/ticket/sync/sync.service.ts b/packages/api/src/ticketing/ticket/sync/sync.service.ts index 8f4520118..ebc004ae0 100644 --- a/packages/api/src/ticketing/ticket/sync/sync.service.ts +++ b/packages/api/src/ticketing/ticket/sync/sync.service.ts @@ -15,6 +15,8 @@ import { ITicketService } from '../types'; import { OriginalTicketOutput } from '@@core/utils/types/original/original.ticketing'; import { ServiceRegistry } from '../services/registry.service'; import { TICKETING_PROVIDERS } from '@panora/shared'; +import { InjectQueue } from '@nestjs/bull'; +import { Queue } from 'bull'; @Injectable() export class SyncService implements OnModuleInit { @@ -24,25 +26,54 @@ export class SyncService implements OnModuleInit { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + @InjectQueue('syncTasks') private syncQueue: Queue, ) { this.logger.setContext(SyncService.name); } async onModuleInit() { try { - await this.syncTickets(); + await this.scheduleSyncJob(); } catch (error) { handleServiceError(error, this.logger); } } - @Cron('*/20 * * * *') + private async scheduleSyncJob() { + const jobName = 'ticketing-sync-tickets'; + + // Remove existing jobs to avoid duplicates in case of application restart + const jobs = await this.syncQueue.getRepeatableJobs(); + for (const job of jobs) { + if (job.name === jobName) { + await this.syncQueue.removeRepeatableByKey(job.key); + } + } + // Add new job to the queue with a CRON expression + await this.syncQueue.add( + jobName, + {}, + { + repeat: { cron: '0 0 * * *' }, // Runs once a day at midnight + }, + ); + } //function used by sync worker which populate our tcg_tickets table //its role is to fetch all contacts from providers 3rd parties and save the info inside our db - async syncTickets() { + //@Cron('*/2 * * * *') // every 2 minutes (for testing) + @Cron('0 */8 * * *') // every 8 hours + async syncTickets(user_id?: string) { try { this.logger.log(`Syncing tickets....`); - const users = await this.prisma.users.findMany(); + const users = user_id + ? [ + await this.prisma.users.findUnique({ + where: { + id_user: user_id, + }, + }), + ] + : await this.prisma.users.findMany(); if (users && users.length > 0) { for (const user of users) { const projects = await this.prisma.projects.findMany({ diff --git a/packages/api/src/ticketing/ticket/ticket.module.ts b/packages/api/src/ticketing/ticket/ticket.module.ts index 7a798c6fe..22195516f 100644 --- a/packages/api/src/ticketing/ticket/ticket.module.ts +++ b/packages/api/src/ticketing/ticket/ticket.module.ts @@ -18,9 +18,12 @@ import { GorgiasService } from './services/gorgias'; @Module({ imports: [ - BullModule.registerQueue({ - name: 'webhookDelivery', - }), + BullModule.registerQueue( + { + name: 'webhookDelivery', + }, + { name: 'syncTasks' }, + ), ], controllers: [TicketController], providers: [ @@ -40,6 +43,13 @@ import { GorgiasService } from './services/gorgias'; JiraService, GorgiasService, ], - exports: [SyncService], + exports: [ + SyncService, + ServiceRegistry, + WebhookService, + FieldMappingService, + LoggerService, + PrismaService, + ], }) export class TicketModule {} diff --git a/packages/api/src/ticketing/ticket/types/model.unified.ts b/packages/api/src/ticketing/ticket/types/model.unified.ts index d704c086e..7c3c31b34 100644 --- a/packages/api/src/ticketing/ticket/types/model.unified.ts +++ b/packages/api/src/ticketing/ticket/types/model.unified.ts @@ -1,82 +1,120 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { UnifiedCommentInput } from '@ticketing/comment/types/model.unified'; +import { IsIn, IsOptional, IsString, IsUUID } from 'class-validator'; export class UnifiedTicketInput { @ApiProperty({ + type: String, description: 'The name of the ticket', }) + @IsString() name: string; @ApiPropertyOptional({ - description: 'The status of the ticket', + type: String, + description: + 'The status of the ticket. Authorized values are OPEN or CLOSED.', }) + @IsIn(['OPEN', 'CLOSED'], { + message: 'Type must be either OPEN or CLOSED', + }) + @IsOptional() status?: string; @ApiProperty({ - type: [String], + type: String, description: 'The description of the ticket', }) + @IsString() description: string; @ApiPropertyOptional({ type: Date, description: 'The date the ticket is due', }) + @IsOptional() due_date?: Date; @ApiPropertyOptional({ - description: 'The type of the ticket', + type: String, + description: + 'The type of the ticket. Authorized values are PROBLEM, QUESTION, or TASK', + }) + @IsIn(['PROBLEM', 'QUESTION', 'TASK'], { + message: 'Type must be either PROBLEM, QUESTION or TASK', }) + @IsOptional() type?: string; @ApiPropertyOptional({ + type: String, description: 'The uuid of the parent ticket', }) + @IsUUID() + @IsOptional() parent_ticket?: string; @ApiPropertyOptional({ type: String, - description: 'The uuid of the project the ticket belongs to', + description: 'The uuid of the collection (project) the ticket belongs to', }) + @IsUUID() + @IsOptional() project_id?: string; @ApiPropertyOptional({ type: [String], description: 'The tags names of the ticket', }) + @IsOptional() tags?: string[]; // tags names @ApiPropertyOptional({ type: Date, description: 'The date the ticket has been completed', }) + @IsOptional() completed_at?: Date; @ApiPropertyOptional({ - description: 'The priority of the ticket', + type: String, + description: + 'The priority of the ticket. Authorized values are HIGH, MEDIUM or LOW.', + }) + @IsIn(['HIGH', 'MEDIUM', 'LOW'], { + message: 'Type must be either HIGH, MEDIUM or LOW', }) + @IsOptional() priority?: string; @ApiPropertyOptional({ type: [String], description: 'The users uuids the ticket is assigned to', }) + @IsOptional() assigned_to?: string[]; //uuid of Users objects ? @ApiPropertyOptional({ type: UnifiedCommentInput, description: 'The comment of the ticket', }) + @IsOptional() comment?: UnifiedCommentInput; @ApiPropertyOptional({ + type: String, description: 'The uuid of the account which the ticket belongs to', }) + @IsUUID() + @IsOptional() account_id?: string; @ApiPropertyOptional({ + type: String, description: 'The uuid of the contact which the ticket belongs to', }) + @IsUUID() + @IsOptional() contact_id?: string; @ApiPropertyOptional({ @@ -84,21 +122,28 @@ export class UnifiedTicketInput { description: 'The custom field mappings of the ticket between the remote 3rd party & Panora', }) + @IsOptional() field_mappings?: Record; } export class UnifiedTicketOutput extends UnifiedTicketInput { - @ApiPropertyOptional({ description: 'The uuid of the ticket' }) + @ApiPropertyOptional({ type: String, description: 'The uuid of the ticket' }) + @IsUUID() + @IsOptional() id?: string; @ApiPropertyOptional({ + type: String, description: 'The id of the ticket in the context of the 3rd Party', }) + @IsString() + @IsOptional() remote_id?: string; @ApiPropertyOptional({ - type: [{}], + type: {}, description: 'The remote data of the ticket in the context of the 3rd Party', }) + @IsOptional() remote_data?: Record; } diff --git a/packages/api/src/ticketing/user/sync/sync.processor.ts b/packages/api/src/ticketing/user/sync/sync.processor.ts new file mode 100644 index 000000000..66fd65fb8 --- /dev/null +++ b/packages/api/src/ticketing/user/sync/sync.processor.ts @@ -0,0 +1,17 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; + +@Processor('syncTasks') +export class SyncProcessor { + constructor(private syncService: SyncService) {} + @Process('ticketing-sync-users') + async handleSyncUsers(job: Job) { + try { + console.log(`Processing queue -> ticketing-sync-users ${job.id}`); + await this.syncService.syncUsers(); + } catch (error) { + console.error('Error syncing ticketing users', error); + } + } +} diff --git a/packages/api/src/ticketing/user/sync/sync.service.ts b/packages/api/src/ticketing/user/sync/sync.service.ts index 3b95a4606..113dcda4e 100644 --- a/packages/api/src/ticketing/user/sync/sync.service.ts +++ b/packages/api/src/ticketing/user/sync/sync.service.ts @@ -15,6 +15,8 @@ import { OriginalUserOutput } from '@@core/utils/types/original/original.ticketi import { tcg_users as TicketingUser } from '@prisma/client'; import { UnifiedUserOutput } from '../types/model.unified'; import { TICKETING_PROVIDERS } from '@panora/shared'; +import { InjectQueue } from '@nestjs/bull'; +import { Queue } from 'bull'; @Injectable() export class SyncService implements OnModuleInit { @@ -24,25 +26,54 @@ export class SyncService implements OnModuleInit { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + @InjectQueue('syncTasks') private syncQueue: Queue, ) { this.logger.setContext(SyncService.name); } async onModuleInit() { try { - await this.syncUsers(); + await this.scheduleSyncJob(); } catch (error) { handleServiceError(error, this.logger); } } - @Cron('*/20 * * * *') + private async scheduleSyncJob() { + const jobName = 'ticketing-sync-users'; + + // Remove existing jobs to avoid duplicates in case of application restart + const jobs = await this.syncQueue.getRepeatableJobs(); + for (const job of jobs) { + if (job.name === jobName) { + await this.syncQueue.removeRepeatableByKey(job.key); + } + } + // Add new job to the queue with a CRON expression + await this.syncQueue.add( + jobName, + {}, + { + repeat: { cron: '0 0 * * *' }, // Runs once a day at midnight + }, + ); + } //function used by sync worker which populate our tcg_users table //its role is to fetch all users from providers 3rd parties and save the info inside our db - async syncUsers() { + //@Cron('*/2 * * * *') // every 2 minutes (for testing) + @Cron('0 */8 * * *') // every 8 hours + async syncUsers(user_id?: string) { try { this.logger.log(`Syncing users....`); - const users = await this.prisma.users.findMany(); + const users = user_id + ? [ + await this.prisma.users.findUnique({ + where: { + id_user: user_id, + }, + }), + ] + : await this.prisma.users.findMany(); if (users && users.length > 0) { for (const user of users) { const projects = await this.prisma.projects.findMany({ diff --git a/packages/api/src/ticketing/user/types/model.unified.ts b/packages/api/src/ticketing/user/types/model.unified.ts index f6af2c595..a4f2447cd 100644 --- a/packages/api/src/ticketing/user/types/model.unified.ts +++ b/packages/api/src/ticketing/user/types/model.unified.ts @@ -1,49 +1,64 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsOptional, IsString, IsUUID } from 'class-validator'; export class UnifiedUserInput { @ApiProperty({ + type: String, description: 'The name of the user', }) + @IsString() name: string; @ApiProperty({ + type: String, description: 'The email address of the user', }) + @IsString() email_address: string; @ApiPropertyOptional({ type: [String], description: 'The teams whose the user is part of', }) + @IsOptional() teams?: string[]; //TODO @ApiPropertyOptional({ - type: [String], + type: String, description: 'The account or organization the user is part of', }) - account_id?: string[]; + @IsUUID() + @IsOptional() + account_id?: string; @ApiProperty({ type: {}, description: 'The custom field mappings of the user between the remote 3rd party & Panora', }) + @IsOptional() field_mappings?: Record; } export class UnifiedUserOutput extends UnifiedUserInput { - @ApiPropertyOptional({ description: 'The uuid of the user' }) + @ApiPropertyOptional({ type: String, description: 'The uuid of the user' }) + @IsUUID() + @IsOptional() id?: string; @ApiPropertyOptional({ + type: String, description: 'The id of the user in the context of the 3rd Party', }) + @IsString() + @IsOptional() remote_id?: string; @ApiPropertyOptional({ - type: [{}], + type: {}, description: 'The remote data of the user in the context of the 3rd Party', }) + @IsOptional() remote_data?: Record; } diff --git a/packages/api/src/ticketing/user/user.module.ts b/packages/api/src/ticketing/user/user.module.ts index 6b5f8c481..f00b064e1 100644 --- a/packages/api/src/ticketing/user/user.module.ts +++ b/packages/api/src/ticketing/user/user.module.ts @@ -16,9 +16,12 @@ import { GorgiasService } from './services/gorgias'; @Module({ imports: [ - BullModule.registerQueue({ - name: 'webhookDelivery', - }), + BullModule.registerQueue( + { + name: 'webhookDelivery', + }, + { name: 'syncTasks' }, + ), ], controllers: [UserController], providers: [ @@ -36,6 +39,13 @@ import { GorgiasService } from './services/gorgias'; JiraService, GorgiasService, ], - exports: [SyncService], + exports: [ + SyncService, + ServiceRegistry, + WebhookService, + FieldMappingService, + LoggerService, + PrismaService, + ], }) export class UserModule {} diff --git a/packages/api/swagger/swagger-spec.json b/packages/api/swagger/swagger-spec.json index 8a93f3700..c00c2d20a 100644 --- a/packages/api/swagger/swagger-spec.json +++ b/packages/api/swagger/swagger-spec.json @@ -176,16 +176,7 @@ "get": { "operationId": "getApiKeys", "summary": "Retrieve API Keys", - "parameters": [ - { - "name": "project_id", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - } - ], + "parameters": [], "responses": { "200": { "description": "" @@ -221,6 +212,31 @@ ] } }, + "/auth/refresh-token": { + "post": { + "operationId": "refreshAccessToken", + "summary": "Refresh Access Token", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefreshDto" + } + } + } + }, + "responses": { + "201": { + "description": "" + } + }, + "tags": [ + "auth" + ] + } + }, "/connections/oauth/callback": { "get": { "operationId": "handleOAuthCallback", @@ -336,16 +352,7 @@ "get": { "operationId": "getConnections", "summary": "List Connections", - "parameters": [ - { - "name": "projectId", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - } - ], + "parameters": [], "responses": { "200": { "description": "" @@ -360,16 +367,7 @@ "get": { "operationId": "getWebhooksMetadata", "summary": "Retrieve webhooks metadata ", - "parameters": [ - { - "name": "project_id", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - } - ], + "parameters": [], "responses": { "200": { "description": "" @@ -456,16 +454,7 @@ "get": { "operationId": "getLinkedUsers", "summary": "Retrieve Linked Users", - "parameters": [ - { - "name": "project_id", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - } - ], + "parameters": [], "responses": { "200": { "description": "" @@ -739,14 +728,6 @@ "default": 10, "type": "number" } - }, - { - "name": "project_id", - "required": true, - "in": "query", - "schema": { - "type": "string" - } } ], "responses": { @@ -1073,11 +1054,26 @@ "get": { "operationId": "getConnectionStrategiesForProject", "summary": "Fetch All Connection Strategies for Project", + "parameters": [], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "connections-strategies" + ] + } + }, + "/sync/status/{vertical}": { + "get": { + "operationId": "getSyncStatus", + "summary": "Retrieve sync status of a certain vertical", "parameters": [ { - "name": "projectId", + "name": "vertical", "required": true, - "in": "query", + "in": "path", "schema": { "type": "string" } @@ -1089,14 +1085,38 @@ } }, "tags": [ - "connections-strategies" + "sync" ] } }, - "/crm/contacts": { + "/sync/resync/{vertical}": { "get": { - "operationId": "getContacts", - "summary": "List a batch of CRM Contacts", + "operationId": "resync", + "summary": "Resync common objects across a vertical", + "parameters": [ + { + "name": "vertical", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "sync" + ] + } + }, + "/crm/companies": { + "get": { + "operationId": "getCompanies", + "summary": "List a batch of Companies", "parameters": [ { "name": "x-connection-token", @@ -1111,7 +1131,7 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original CRM software.", + "description": "Set to true to include data from the original Crm software.", "schema": { "type": "boolean" } @@ -1130,7 +1150,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedContactOutput" + "$ref": "#/components/schemas/UnifiedCompanyOutput" } } } @@ -1141,13 +1161,13 @@ } }, "tags": [ - "crm/contacts" + "crm/companies" ] }, "post": { - "operationId": "addContact", - "summary": "Create CRM Contact", - "description": "Create a contact in any supported CRM", + "operationId": "addCompany", + "summary": "Create a Company", + "description": "Create a company in any supported Crm software", "parameters": [ { "name": "x-connection-token", @@ -1162,7 +1182,7 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original CRM software.", + "description": "Set to true to include data from the original Crm software.", "schema": { "type": "boolean" } @@ -1173,7 +1193,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnifiedContactInput" + "$ref": "#/components/schemas/UnifiedCompanyInput" } } } @@ -1191,7 +1211,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedContactOutput" + "$ref": "#/components/schemas/UnifiedCompanyOutput" } } } @@ -1205,19 +1225,19 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnifiedContactOutput" + "$ref": "#/components/schemas/UnifiedCompanyOutput" } } } } }, "tags": [ - "crm/contacts" + "crm/companies" ] }, "patch": { - "operationId": "updateContact", - "summary": "Update a CRM Contact", + "operationId": "updateCompany", + "summary": "Update a Company", "parameters": [ { "name": "id", @@ -1234,28 +1254,39 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnifiedContactOutput" + "allOf": [ + { + "$ref": "#/components/schemas/ApiResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/UnifiedCompanyOutput" + } + } + } + ] } } } } }, "tags": [ - "crm/contacts" + "crm/companies" ] } }, - "/crm/contacts/{id}": { + "/crm/companies/{id}": { "get": { - "operationId": "getContact", - "summary": "Retrieve a CRM Contact", - "description": "Retrieve a contact from any connected CRM", + "operationId": "getCompany", + "summary": "Retrieve a Company", + "description": "Retrieve a company from any connected Crm software", "parameters": [ { "name": "id", "required": true, "in": "path", - "description": "id of the `contact` you want to retrive.", + "description": "id of the company you want to retrieve.", "schema": { "type": "string" } @@ -1264,7 +1295,7 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original CRM software.", + "description": "Set to true to include data from the original Crm software.", "schema": { "type": "boolean" } @@ -1283,7 +1314,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedContactOutput" + "$ref": "#/components/schemas/UnifiedCompanyOutput" } } } @@ -1294,14 +1325,14 @@ } }, "tags": [ - "crm/contacts" + "crm/companies" ] } }, - "/crm/contacts/batch": { + "/crm/companies/batch": { "post": { - "operationId": "addContacts", - "summary": "Add a batch of CRM Contacts", + "operationId": "addCompanies", + "summary": "Add a batch of Companies", "parameters": [ { "name": "x-connection-token", @@ -1316,7 +1347,7 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original CRM software.", + "description": "Set to true to include data from the original Crm software.", "schema": { "type": "boolean" } @@ -1329,7 +1360,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/UnifiedContactInput" + "$ref": "#/components/schemas/UnifiedCompanyInput" } } } @@ -1348,7 +1379,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedContactOutput" + "$ref": "#/components/schemas/UnifiedCompanyOutput" } } } @@ -1364,7 +1395,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/UnifiedContactOutput" + "$ref": "#/components/schemas/UnifiedCompanyOutput" } } } @@ -1372,14 +1403,14 @@ } }, "tags": [ - "crm/contacts" + "crm/companies" ] } }, - "/crm/deals": { + "/crm/contacts": { "get": { - "operationId": "getDeals", - "summary": "List a batch of Deals", + "operationId": "getContacts", + "summary": "List a batch of CRM Contacts", "parameters": [ { "name": "x-connection-token", @@ -1394,7 +1425,7 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Crm software.", + "description": "Set to true to include data from the original CRM software.", "schema": { "type": "boolean" } @@ -1413,7 +1444,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedDealOutput" + "$ref": "#/components/schemas/UnifiedContactOutput" } } } @@ -1424,13 +1455,13 @@ } }, "tags": [ - "crm/deals" + "crm/contacts" ] }, "post": { - "operationId": "addDeal", - "summary": "Create a Deal", - "description": "Create a deal in any supported Crm software", + "operationId": "addContact", + "summary": "Create CRM Contact", + "description": "Create a contact in any supported CRM", "parameters": [ { "name": "x-connection-token", @@ -1445,7 +1476,7 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Crm software.", + "description": "Set to true to include data from the original CRM software.", "schema": { "type": "boolean" } @@ -1456,7 +1487,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnifiedDealInput" + "$ref": "#/components/schemas/UnifiedContactInput" } } } @@ -1474,7 +1505,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedDealOutput" + "$ref": "#/components/schemas/UnifiedContactOutput" } } } @@ -1488,28 +1519,57 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnifiedDealOutput" + "$ref": "#/components/schemas/UnifiedContactOutput" } } } } }, "tags": [ - "crm/deals" + "crm/contacts" + ] + }, + "patch": { + "operationId": "updateContact", + "summary": "Update a CRM Contact", + "parameters": [ + { + "name": "id", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnifiedContactOutput" + } + } + } + } + }, + "tags": [ + "crm/contacts" ] } }, - "/crm/deals/{id}": { + "/crm/contacts/{id}": { "get": { - "operationId": "getDeal", - "summary": "Retrieve a Deal", - "description": "Retrieve a deal from any connected Crm software", + "operationId": "getContact", + "summary": "Retrieve a CRM Contact", + "description": "Retrieve a contact from any connected CRM", "parameters": [ { "name": "id", "required": true, "in": "path", - "description": "id of the deal you want to retrieve.", + "description": "id of the `contact` you want to retrive.", "schema": { "type": "string" } @@ -1518,7 +1578,7 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Crm software.", + "description": "Set to true to include data from the original CRM software.", "schema": { "type": "boolean" } @@ -1537,7 +1597,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedDealOutput" + "$ref": "#/components/schemas/UnifiedContactOutput" } } } @@ -1548,54 +1608,14 @@ } }, "tags": [ - "crm/deals" - ] - }, - "patch": { - "operationId": "updateDeal", - "summary": "Update a Deal", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ApiResponse" - }, - { - "properties": { - "data": { - "$ref": "#/components/schemas/UnifiedDealOutput" - } - } - } - ] - } - } - } - } - }, - "tags": [ - "crm/deals" + "crm/contacts" ] } }, - "/crm/deals/batch": { + "/crm/contacts/batch": { "post": { - "operationId": "addDeals", - "summary": "Add a batch of Deals", + "operationId": "addContacts", + "summary": "Add a batch of CRM Contacts", "parameters": [ { "name": "x-connection-token", @@ -1610,7 +1630,7 @@ "name": "remote_data", "required": false, "in": "query", - "description": "Set to true to include data from the original Crm software.", + "description": "Set to true to include data from the original CRM software.", "schema": { "type": "boolean" } @@ -1623,7 +1643,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/UnifiedDealInput" + "$ref": "#/components/schemas/UnifiedContactInput" } } } @@ -1642,7 +1662,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedDealOutput" + "$ref": "#/components/schemas/UnifiedContactOutput" } } } @@ -1658,7 +1678,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/UnifiedDealOutput" + "$ref": "#/components/schemas/UnifiedContactOutput" } } } @@ -1666,14 +1686,14 @@ } }, "tags": [ - "crm/deals" + "crm/contacts" ] } }, - "/crm/notes": { + "/crm/deals": { "get": { - "operationId": "getNotes", - "summary": "List a batch of Notes", + "operationId": "getDeals", + "summary": "List a batch of Deals", "parameters": [ { "name": "x-connection-token", @@ -1707,7 +1727,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedNoteOutput" + "$ref": "#/components/schemas/UnifiedDealOutput" } } } @@ -1718,13 +1738,13 @@ } }, "tags": [ - "crm/notes" + "crm/deals" ] }, "post": { - "operationId": "addNote", - "summary": "Create a Note", - "description": "Create a note in any supported Crm software", + "operationId": "addDeal", + "summary": "Create a Deal", + "description": "Create a deal in any supported Crm software", "parameters": [ { "name": "x-connection-token", @@ -1750,7 +1770,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnifiedNoteInput" + "$ref": "#/components/schemas/UnifiedDealInput" } } } @@ -1768,7 +1788,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedNoteOutput" + "$ref": "#/components/schemas/UnifiedDealOutput" } } } @@ -1782,28 +1802,28 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnifiedNoteOutput" + "$ref": "#/components/schemas/UnifiedDealOutput" } } } } }, "tags": [ - "crm/notes" + "crm/deals" ] } }, - "/crm/notes/{id}": { + "/crm/deals/{id}": { "get": { - "operationId": "getNote", - "summary": "Retrieve a Note", - "description": "Retrieve a note from any connected Crm software", + "operationId": "getDeal", + "summary": "Retrieve a Deal", + "description": "Retrieve a deal from any connected Crm software", "parameters": [ { "name": "id", "required": true, "in": "path", - "description": "id of the note you want to retrieve.", + "description": "id of the deal you want to retrieve.", "schema": { "type": "string" } @@ -1831,7 +1851,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedNoteOutput" + "$ref": "#/components/schemas/UnifiedDealOutput" } } } @@ -1842,14 +1862,54 @@ } }, "tags": [ - "crm/notes" + "crm/deals" + ] + }, + "patch": { + "operationId": "updateDeal", + "summary": "Update a Deal", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/UnifiedDealOutput" + } + } + } + ] + } + } + } + } + }, + "tags": [ + "crm/deals" ] } }, - "/crm/notes/batch": { + "/crm/deals/batch": { "post": { - "operationId": "addNotes", - "summary": "Add a batch of Notes", + "operationId": "addDeals", + "summary": "Add a batch of Deals", "parameters": [ { "name": "x-connection-token", @@ -1877,7 +1937,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/UnifiedNoteInput" + "$ref": "#/components/schemas/UnifiedDealInput" } } } @@ -1896,7 +1956,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedNoteOutput" + "$ref": "#/components/schemas/UnifiedDealOutput" } } } @@ -1912,7 +1972,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/UnifiedNoteOutput" + "$ref": "#/components/schemas/UnifiedDealOutput" } } } @@ -1920,14 +1980,14 @@ } }, "tags": [ - "crm/notes" + "crm/deals" ] } }, - "/crm/companies": { + "/crm/engagements": { "get": { - "operationId": "getCompanies", - "summary": "List a batch of Companies", + "operationId": "getEngagements", + "summary": "List a batch of Engagements", "parameters": [ { "name": "x-connection-token", @@ -1961,7 +2021,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedCompanyOutput" + "$ref": "#/components/schemas/UnifiedEngagementOutput" } } } @@ -1972,13 +2032,13 @@ } }, "tags": [ - "crm/companies" + "crm/engagements" ] }, "post": { - "operationId": "addCompany", - "summary": "Create a Company", - "description": "Create a company in any supported Crm software", + "operationId": "addEngagement", + "summary": "Create a Engagement", + "description": "Create a engagement in any supported Crm software", "parameters": [ { "name": "x-connection-token", @@ -2004,7 +2064,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnifiedCompanyInput" + "$ref": "#/components/schemas/UnifiedEngagementInput" } } } @@ -2022,7 +2082,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedCompanyOutput" + "$ref": "#/components/schemas/UnifiedEngagementOutput" } } } @@ -2036,19 +2096,19 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnifiedCompanyOutput" + "$ref": "#/components/schemas/UnifiedEngagementOutput" } } } } }, "tags": [ - "crm/companies" + "crm/engagements" ] }, "patch": { - "operationId": "updateCompany", - "summary": "Update a Company", + "operationId": "updateEngagement", + "summary": "Update a Engagement", "parameters": [ { "name": "id", @@ -2072,7 +2132,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedCompanyOutput" + "$ref": "#/components/schemas/UnifiedEngagementOutput" } } } @@ -2083,21 +2143,21 @@ } }, "tags": [ - "crm/companies" + "crm/engagements" ] } }, - "/crm/companies/{id}": { + "/crm/engagements/{id}": { "get": { - "operationId": "getCompany", - "summary": "Retrieve a Company", - "description": "Retrieve a company from any connected Crm software", + "operationId": "getEngagement", + "summary": "Retrieve a Engagement", + "description": "Retrieve a engagement from any connected Crm software", "parameters": [ { "name": "id", "required": true, "in": "path", - "description": "id of the company you want to retrieve.", + "description": "id of the engagement you want to retrieve.", "schema": { "type": "string" } @@ -2125,7 +2185,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedCompanyOutput" + "$ref": "#/components/schemas/UnifiedEngagementOutput" } } } @@ -2136,14 +2196,14 @@ } }, "tags": [ - "crm/companies" + "crm/engagements" ] } }, - "/crm/companies/batch": { + "/crm/engagements/batch": { "post": { - "operationId": "addCompanies", - "summary": "Add a batch of Companies", + "operationId": "addEngagements", + "summary": "Add a batch of Engagements", "parameters": [ { "name": "x-connection-token", @@ -2171,7 +2231,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/UnifiedCompanyInput" + "$ref": "#/components/schemas/UnifiedEngagementInput" } } } @@ -2190,7 +2250,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedCompanyOutput" + "$ref": "#/components/schemas/UnifiedEngagementOutput" } } } @@ -2206,7 +2266,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/UnifiedCompanyOutput" + "$ref": "#/components/schemas/UnifiedEngagementOutput" } } } @@ -2214,14 +2274,14 @@ } }, "tags": [ - "crm/companies" + "crm/engagements" ] } }, - "/crm/engagements": { + "/crm/notes": { "get": { - "operationId": "getEngagements", - "summary": "List a batch of Engagements", + "operationId": "getNotes", + "summary": "List a batch of Notes", "parameters": [ { "name": "x-connection-token", @@ -2255,7 +2315,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedEngagementOutput" + "$ref": "#/components/schemas/UnifiedNoteOutput" } } } @@ -2266,13 +2326,13 @@ } }, "tags": [ - "crm/engagements" + "crm/notes" ] }, "post": { - "operationId": "addEngagement", - "summary": "Create a Engagement", - "description": "Create a engagement in any supported Crm software", + "operationId": "addNote", + "summary": "Create a Note", + "description": "Create a note in any supported Crm software", "parameters": [ { "name": "x-connection-token", @@ -2298,7 +2358,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnifiedEngagementInput" + "$ref": "#/components/schemas/UnifiedNoteInput" } } } @@ -2316,7 +2376,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedEngagementOutput" + "$ref": "#/components/schemas/UnifiedNoteOutput" } } } @@ -2330,68 +2390,28 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnifiedEngagementOutput" + "$ref": "#/components/schemas/UnifiedNoteOutput" } } } } }, "tags": [ - "crm/engagements" + "crm/notes" ] - }, - "patch": { - "operationId": "updateEngagement", - "summary": "Update a Engagement", + } + }, + "/crm/notes/{id}": { + "get": { + "operationId": "getNote", + "summary": "Retrieve a Note", + "description": "Retrieve a note from any connected Crm software", "parameters": [ { "name": "id", "required": true, - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ApiResponse" - }, - { - "properties": { - "data": { - "$ref": "#/components/schemas/UnifiedEngagementOutput" - } - } - } - ] - } - } - } - } - }, - "tags": [ - "crm/engagements" - ] - } - }, - "/crm/engagements/{id}": { - "get": { - "operationId": "getEngagement", - "summary": "Retrieve a Engagement", - "description": "Retrieve a engagement from any connected Crm software", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "description": "id of the engagement you want to retrieve.", + "in": "path", + "description": "id of the note you want to retrieve.", "schema": { "type": "string" } @@ -2419,7 +2439,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedEngagementOutput" + "$ref": "#/components/schemas/UnifiedNoteOutput" } } } @@ -2430,14 +2450,14 @@ } }, "tags": [ - "crm/engagements" + "crm/notes" ] } }, - "/crm/engagements/batch": { + "/crm/notes/batch": { "post": { - "operationId": "addEngagements", - "summary": "Add a batch of Engagements", + "operationId": "addNotes", + "summary": "Add a batch of Notes", "parameters": [ { "name": "x-connection-token", @@ -2465,7 +2485,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/UnifiedEngagementInput" + "$ref": "#/components/schemas/UnifiedNoteInput" } } } @@ -2484,7 +2504,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedEngagementOutput" + "$ref": "#/components/schemas/UnifiedNoteOutput" } } } @@ -2500,7 +2520,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/UnifiedEngagementOutput" + "$ref": "#/components/schemas/UnifiedNoteOutput" } } } @@ -2508,7 +2528,7 @@ } }, "tags": [ - "crm/engagements" + "crm/notes" ] } }, @@ -3016,10 +3036,10 @@ ] } }, - "/ticketing/tickets": { + "/ticketing/accounts": { "get": { - "operationId": "getTickets", - "summary": "List a batch of Tickets", + "operationId": "getAccounts", + "summary": "List a batch of Accounts", "parameters": [ { "name": "x-connection-token", @@ -3053,7 +3073,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedTicketOutput" + "$ref": "#/components/schemas/UnifiedAccountOutput" } } } @@ -3064,19 +3084,21 @@ } }, "tags": [ - "ticketing/tickets" + "ticketing/accounts" ] - }, - "post": { - "operationId": "addTicket", - "summary": "Create a Ticket", - "description": "Create a ticket in any supported Ticketing software", + } + }, + "/ticketing/accounts/{id}": { + "get": { + "operationId": "getAccount", + "summary": "Retrieve an Account", + "description": "Retrieve an account from any connected Ticketing software", "parameters": [ { - "name": "x-connection-token", + "name": "id", "required": true, - "in": "header", - "description": "The connection token", + "in": "path", + "description": "id of the account you want to retrieve.", "schema": { "type": "string" } @@ -3091,16 +3113,6 @@ } } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnifiedTicketInput" - } - } - } - }, "responses": { "200": { "description": "", @@ -3114,7 +3126,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedTicketOutput" + "$ref": "#/components/schemas/UnifiedAccountOutput" } } } @@ -3122,63 +3134,23 @@ } } } - }, - "201": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnifiedTicketOutput" - } - } - } - } - }, - "tags": [ - "ticketing/tickets" - ] - }, - "patch": { - "operationId": "updateTicket", - "summary": "Update a Ticket", - "parameters": [ - { - "name": "id", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnifiedTicketOutput" - } - } - } } }, "tags": [ - "ticketing/tickets" + "ticketing/accounts" ] } }, - "/ticketing/tickets/{id}": { + "/ticketing/collection": { "get": { - "operationId": "getTicket", - "summary": "Retrieve a Ticket", - "description": "Retrieve a ticket from any connected Ticketing software", + "operationId": "getCollections", + "summary": "List a batch of Collections", "parameters": [ { - "name": "id", + "name": "x-connection-token", "required": true, - "in": "path", - "description": "id of the `ticket` you want to retrive.", + "in": "header", + "description": "The connection token", "schema": { "type": "string" } @@ -3206,7 +3178,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedTicketOutput" + "$ref": "#/components/schemas/UnifiedCollectionOutput" } } } @@ -3217,20 +3189,21 @@ } }, "tags": [ - "ticketing/tickets" + "ticketing/collection" ] } }, - "/ticketing/tickets/batch": { - "post": { - "operationId": "addTickets", - "summary": "Add a batch of Tickets", + "/ticketing/collection/{id}": { + "get": { + "operationId": "getCollection", + "summary": "Retrieve a Collection", + "description": "Retrieve a collection from any connected Ticketing software", "parameters": [ { - "name": "x-connection-token", + "name": "id", "required": true, - "in": "header", - "description": "The connection token", + "in": "path", + "description": "id of the collection you want to retrieve.", "schema": { "type": "string" } @@ -3245,19 +3218,6 @@ } } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/UnifiedTicketInput" - } - } - } - } - }, "responses": { "200": { "description": "", @@ -3271,7 +3231,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedTicketOutput" + "$ref": "#/components/schemas/UnifiedCollectionOutput" } } } @@ -3279,23 +3239,10 @@ } } } - }, - "201": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/UnifiedTicketOutput" - } - } - } - } } }, "tags": [ - "ticketing/tickets" + "ticketing/collection" ] } }, @@ -3553,10 +3500,10 @@ ] } }, - "/ticketing/users": { + "/ticketing/contacts": { "get": { - "operationId": "getUsers", - "summary": "List a batch of Users", + "operationId": "getContacts", + "summary": "List a batch of Contacts", "parameters": [ { "name": "x-connection-token", @@ -3590,7 +3537,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedUserOutput" + "$ref": "#/components/schemas/UnifiedContactOutput" } } } @@ -3601,21 +3548,21 @@ } }, "tags": [ - "ticketing/users" + "ticketing/contacts" ] } }, - "/ticketing/users/{id}": { + "/ticketing/contacts/{id}": { "get": { - "operationId": "getUser", - "summary": "Retrieve a User", - "description": "Retrieve a user from any connected Ticketing software", + "operationId": "getContact", + "summary": "Retrieve a Contact", + "description": "Retrieve a contact from any connected Ticketing software", "parameters": [ { "name": "id", "required": true, "in": "path", - "description": "id of the user you want to retrieve.", + "description": "id of the contact you want to retrieve.", "schema": { "type": "string" } @@ -3643,7 +3590,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedUserOutput" + "$ref": "#/components/schemas/UnifiedContactOutput" } } } @@ -3654,14 +3601,14 @@ } }, "tags": [ - "ticketing/users" + "ticketing/contacts" ] } }, - "/ticketing/attachments": { + "/ticketing/tags": { "get": { - "operationId": "getAttachments", - "summary": "List a batch of Attachments", + "operationId": "getTags", + "summary": "List a batch of Tags", "parameters": [ { "name": "x-connection-token", @@ -3695,7 +3642,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedAttachmentOutput" + "$ref": "#/components/schemas/UnifiedTagOutput" } } } @@ -3706,19 +3653,21 @@ } }, "tags": [ - "ticketing/attachments" + "ticketing/tags" ] - }, - "post": { - "operationId": "addAttachment", - "summary": "Create a Attachment", - "description": "Create a attachment in any supported Ticketing software", + } + }, + "/ticketing/tags/{id}": { + "get": { + "operationId": "getTag", + "summary": "Retrieve a Tag", + "description": "Retrieve a tag from any connected Ticketing software", "parameters": [ { - "name": "x-connection-token", + "name": "id", "required": true, - "in": "header", - "description": "The connection token", + "in": "path", + "description": "id of the tag you want to retrieve.", "schema": { "type": "string" } @@ -3733,16 +3682,6 @@ } } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnifiedAttachmentInput" - } - } - } - }, "responses": { "200": { "description": "", @@ -3756,7 +3695,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedAttachmentOutput" + "$ref": "#/components/schemas/UnifiedTagOutput" } } } @@ -3764,34 +3703,23 @@ } } } - }, - "201": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnifiedAttachmentOutput" - } - } - } } }, "tags": [ - "ticketing/attachments" + "ticketing/tags" ] } }, - "/ticketing/attachments/{id}": { + "/ticketing/teams": { "get": { - "operationId": "getAttachment", - "summary": "Retrieve a Attachment", - "description": "Retrieve a attachment from any connected Ticketing software", + "operationId": "getTeams", + "summary": "List a batch of Teams", "parameters": [ { - "name": "id", + "name": "x-connection-token", "required": true, - "in": "path", - "description": "id of the attachment you want to retrive.", + "in": "header", + "description": "The connection token", "schema": { "type": "string" } @@ -3819,7 +3747,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedAttachmentOutput" + "$ref": "#/components/schemas/UnifiedTeamOutput" } } } @@ -3830,21 +3758,21 @@ } }, "tags": [ - "ticketing/attachments" + "ticketing/teams" ] } }, - "/ticketing/attachments/{id}/download": { + "/ticketing/teams/{id}": { "get": { - "operationId": "downloadAttachment", - "summary": "Download a Attachment", - "description": "Download a attachment from any connected Ticketing software", + "operationId": "getTeam", + "summary": "Retrieve a Team", + "description": "Retrieve a team from any connected Ticketing software", "parameters": [ { "name": "id", "required": true, "in": "path", - "description": "id of the attachment you want to retrive.", + "description": "id of the team you want to retrieve.", "schema": { "type": "string" } @@ -3872,7 +3800,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedAttachmentOutput" + "$ref": "#/components/schemas/UnifiedTeamOutput" } } } @@ -3883,14 +3811,14 @@ } }, "tags": [ - "ticketing/attachments" + "ticketing/teams" ] } }, - "/ticketing/attachments/batch": { - "post": { - "operationId": "addAttachments", - "summary": "Add a batch of Attachments", + "/ticketing/tickets": { + "get": { + "operationId": "getTickets", + "summary": "List a batch of Tickets", "parameters": [ { "name": "x-connection-token", @@ -3911,19 +3839,6 @@ } } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/UnifiedAttachmentInput" - } - } - } - } - }, "responses": { "200": { "description": "", @@ -3937,7 +3852,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedAttachmentOutput" + "$ref": "#/components/schemas/UnifiedTicketOutput" } } } @@ -3945,30 +3860,16 @@ } } } - }, - "201": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/UnifiedAttachmentOutput" - } - } - } - } } }, "tags": [ - "ticketing/attachments" + "ticketing/tickets" ] - } - }, - "/ticketing/contacts": { - "get": { - "operationId": "getContacts", - "summary": "List a batch of Contacts", + }, + "post": { + "operationId": "addTicket", + "summary": "Create a Ticket", + "description": "Create a ticket in any supported Ticketing software", "parameters": [ { "name": "x-connection-token", @@ -3989,6 +3890,16 @@ } } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnifiedTicketInput" + } + } + } + }, "responses": { "200": { "description": "", @@ -4002,7 +3913,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedContactOutput" + "$ref": "#/components/schemas/UnifiedTicketOutput" } } } @@ -4010,35 +3921,32 @@ } } } + }, + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnifiedTicketOutput" + } + } + } } }, "tags": [ - "ticketing/contacts" + "ticketing/tickets" ] - } - }, - "/ticketing/contacts/{id}": { - "get": { - "operationId": "getContact", - "summary": "Retrieve a Contact", - "description": "Retrieve a contact from any connected Ticketing software", + }, + "patch": { + "operationId": "updateTicket", + "summary": "Update a Ticket", "parameters": [ { "name": "id", "required": true, - "in": "path", - "description": "id of the contact you want to retrieve.", - "schema": { - "type": "string" - } - }, - { - "name": "remote_data", - "required": false, "in": "query", - "description": "Set to true to include data from the original Ticketing software.", "schema": { - "type": "boolean" + "type": "string" } } ], @@ -4048,38 +3956,28 @@ "content": { "application/json": { "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ApiResponse" - }, - { - "properties": { - "data": { - "$ref": "#/components/schemas/UnifiedContactOutput" - } - } - } - ] + "$ref": "#/components/schemas/UnifiedTicketOutput" } } } } }, "tags": [ - "ticketing/contacts" + "ticketing/tickets" ] } }, - "/ticketing/accounts": { + "/ticketing/tickets/{id}": { "get": { - "operationId": "getAccounts", - "summary": "List a batch of Accounts", + "operationId": "getTicket", + "summary": "Retrieve a Ticket", + "description": "Retrieve a ticket from any connected Ticketing software", "parameters": [ { - "name": "x-connection-token", + "name": "id", "required": true, - "in": "header", - "description": "The connection token", + "in": "path", + "description": "id of the `ticket` you want to retrive.", "schema": { "type": "string" } @@ -4107,7 +4005,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedAccountOutput" + "$ref": "#/components/schemas/UnifiedTicketOutput" } } } @@ -4118,21 +4016,20 @@ } }, "tags": [ - "ticketing/accounts" + "ticketing/tickets" ] } }, - "/ticketing/accounts/{id}": { - "get": { - "operationId": "getAccount", - "summary": "Retrieve an Account", - "description": "Retrieve an account from any connected Ticketing software", + "/ticketing/tickets/batch": { + "post": { + "operationId": "addTickets", + "summary": "Add a batch of Tickets", "parameters": [ { - "name": "id", + "name": "x-connection-token", "required": true, - "in": "path", - "description": "id of the account you want to retrieve.", + "in": "header", + "description": "The connection token", "schema": { "type": "string" } @@ -4147,6 +4044,19 @@ } } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UnifiedTicketInput" + } + } + } + } + }, "responses": { "200": { "description": "", @@ -4160,7 +4070,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedAccountOutput" + "$ref": "#/components/schemas/UnifiedTicketOutput" } } } @@ -4168,17 +4078,30 @@ } } } + }, + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UnifiedTicketOutput" + } + } + } + } } }, "tags": [ - "ticketing/accounts" + "ticketing/tickets" ] } }, - "/ticketing/tags": { + "/ticketing/users": { "get": { - "operationId": "getTags", - "summary": "List a batch of Tags", + "operationId": "getUsers", + "summary": "List a batch of Users", "parameters": [ { "name": "x-connection-token", @@ -4212,7 +4135,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedTagOutput" + "$ref": "#/components/schemas/UnifiedUserOutput" } } } @@ -4223,21 +4146,21 @@ } }, "tags": [ - "ticketing/tags" + "ticketing/users" ] } }, - "/ticketing/tags/{id}": { + "/ticketing/users/{id}": { "get": { - "operationId": "getTag", - "summary": "Retrieve a Tag", - "description": "Retrieve a tag from any connected Ticketing software", + "operationId": "getUser", + "summary": "Retrieve a User", + "description": "Retrieve a user from any connected Ticketing software", "parameters": [ { "name": "id", "required": true, "in": "path", - "description": "id of the tag you want to retrieve.", + "description": "id of the user you want to retrieve.", "schema": { "type": "string" } @@ -4265,7 +4188,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedTagOutput" + "$ref": "#/components/schemas/UnifiedUserOutput" } } } @@ -4276,14 +4199,14 @@ } }, "tags": [ - "ticketing/tags" + "ticketing/users" ] } }, - "/ticketing/teams": { + "/ticketing/attachments": { "get": { - "operationId": "getTeams", - "summary": "List a batch of Teams", + "operationId": "getAttachments", + "summary": "List a batch of Attachments", "parameters": [ { "name": "x-connection-token", @@ -4317,7 +4240,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedTeamOutput" + "$ref": "#/components/schemas/UnifiedAttachmentOutput" } } } @@ -4328,21 +4251,92 @@ } }, "tags": [ - "ticketing/teams" + "ticketing/attachments" ] - } + }, + "post": { + "operationId": "addAttachment", + "summary": "Create a Attachment", + "description": "Create a attachment in any supported Ticketing software", + "parameters": [ + { + "name": "x-connection-token", + "required": true, + "in": "header", + "description": "The connection token", + "schema": { + "type": "string" + } + }, + { + "name": "remote_data", + "required": false, + "in": "query", + "description": "Set to true to include data from the original Ticketing software.", + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnifiedAttachmentInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/UnifiedAttachmentOutput" + } + } + } + ] + } + } + } + }, + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnifiedAttachmentOutput" + } + } + } + } + }, + "tags": [ + "ticketing/attachments" + ] + } }, - "/ticketing/teams/{id}": { + "/ticketing/attachments/{id}": { "get": { - "operationId": "getTeam", - "summary": "Retrieve a Team", - "description": "Retrieve a team from any connected Ticketing software", + "operationId": "getAttachment", + "summary": "Retrieve a Attachment", + "description": "Retrieve a attachment from any connected Ticketing software", "parameters": [ { "name": "id", "required": true, "in": "path", - "description": "id of the team you want to retrieve.", + "description": "id of the attachment you want to retrive.", "schema": { "type": "string" } @@ -4370,7 +4364,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedTeamOutput" + "$ref": "#/components/schemas/UnifiedAttachmentOutput" } } } @@ -4381,20 +4375,21 @@ } }, "tags": [ - "ticketing/teams" + "ticketing/attachments" ] } }, - "/ticketing/collection": { + "/ticketing/attachments/{id}/download": { "get": { - "operationId": "getCollections", - "summary": "List a batch of Collections", + "operationId": "downloadAttachment", + "summary": "Download a Attachment", + "description": "Download a attachment from any connected Ticketing software", "parameters": [ { - "name": "x-connection-token", + "name": "id", "required": true, - "in": "header", - "description": "The connection token", + "in": "path", + "description": "id of the attachment you want to retrive.", "schema": { "type": "string" } @@ -4422,7 +4417,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedCollectionOutput" + "$ref": "#/components/schemas/UnifiedAttachmentOutput" } } } @@ -4433,21 +4428,20 @@ } }, "tags": [ - "ticketing/collection" + "ticketing/attachments" ] } }, - "/ticketing/collection/{id}": { - "get": { - "operationId": "getCollection", - "summary": "Retrieve a Collection", - "description": "Retrieve a collection from any connected Ticketing software", + "/ticketing/attachments/batch": { + "post": { + "operationId": "addAttachments", + "summary": "Add a batch of Attachments", "parameters": [ { - "name": "id", + "name": "x-connection-token", "required": true, - "in": "path", - "description": "id of the collection you want to retrieve.", + "in": "header", + "description": "The connection token", "schema": { "type": "string" } @@ -4462,6 +4456,19 @@ } } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UnifiedAttachmentInput" + } + } + } + } + }, "responses": { "200": { "description": "", @@ -4475,7 +4482,7 @@ { "properties": { "data": { - "$ref": "#/components/schemas/UnifiedCollectionOutput" + "$ref": "#/components/schemas/UnifiedAttachmentOutput" } } } @@ -4483,10 +4490,23 @@ } } } + }, + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UnifiedAttachmentOutput" + } + } + } + } } }, "tags": [ - "ticketing/collection" + "ticketing/attachments" ] } } @@ -4590,6 +4610,17 @@ "keyName" ] }, + "RefreshDto": { + "type": "object", + "properties": { + "projectId": { + "type": "string" + } + }, + "required": [ + "projectId" + ] + }, "WebhookDto": { "type": "object", "properties": { @@ -4911,7 +4942,7 @@ }, "email_address_type": { "type": "string", - "description": "The email address type" + "description": "The email address type. Authorized values are either PERSONAL or WORK." }, "owner_type": { "type": "string", @@ -4923,27 +4954,6 @@ "email_address_type" ] }, - "Phone": { - "type": "object", - "properties": { - "phone_number": { - "type": "string", - "description": "The phone number" - }, - "phone_type": { - "type": "string", - "description": "The phone type" - }, - "owner_type": { - "type": "string", - "description": "The owner type of a phone number" - } - }, - "required": [ - "phone_number", - "phone_type" - ] - }, "Address": { "type": "object", "properties": { @@ -4969,11 +4979,11 @@ }, "country": { "type": "string", - "description": "The country" + "description": "The country." }, "address_type": { "type": "string", - "description": "The address type" + "description": "The address type. Authorized values are either PERSONAL or WORK." }, "owner_type": { "type": "string", @@ -4991,24 +5001,66 @@ "owner_type" ] }, - "UnifiedContactOutput": { + "Phone": { "type": "object", "properties": { - "name": { + "phone_number": { "type": "string", - "description": "The name of the contact" + "description": "The phone number starting with a plus (+) followed by the country code (e.g +336676778890 for France)" }, - "email_address": { + "phone_type": { "type": "string", - "description": "The email address of the contact" + "description": "The phone type. Authorized values are either MOBILE or WORK" }, - "phone_number": { + "owner_type": { "type": "string", - "description": "The phone number of the contact" + "description": "The owner type of a phone number" + } + }, + "required": [ + "phone_number", + "phone_type" + ] + }, + "UnifiedCompanyOutput": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the company" }, - "details": { + "industry": { "type": "string", - "description": "The details of the contact" + "description": "The industry of the company. Authorized values can be found in the Industry enum." + }, + "number_of_employees": { + "type": "number", + "description": "The number of employees of the company" + }, + "user_id": { + "type": "string", + "description": "The uuid of the user who owns the company" + }, + "email_addresses": { + "description": "The email addresses of the company", + "type": "array", + "items": { + "$ref": "#/components/schemas/Email" + } + }, + "addresses": { + "description": "The addresses of the company", + "type": "array", + "items": { + "$ref": "#/components/schemas/Address" + } + }, + "phone_numbers": { + "description": "The phone numbers of the company", + "type": "array", + "items": { + "$ref": "#/components/schemas/Phone" + } }, "field_mappings": { "type": "object", @@ -5016,11 +5068,11 @@ }, "id": { "type": "string", - "description": "The uuid of the contact" + "description": "The uuid of the company" }, "remote_id": { "type": "string", - "description": "The id of the contact in the context of the 3rd Party" + "description": "The id of the company in the context of the Crm 3rd Party" }, "remote_data": { "type": "object", @@ -5029,46 +5081,49 @@ }, "required": [ "name", - "email_address", "field_mappings", "remote_data" ] }, - "UnifiedContactInput": { + "UnifiedCompanyInput": { "type": "object", "properties": { - "first_name": { + "name": { "type": "string", - "description": "The first name of the contact" + "description": "The name of the company" }, - "last_name": { + "industry": { "type": "string", - "description": "The last name of the contact" + "description": "The industry of the company. Authorized values can be found in the Industry enum." }, - "email_addresses": { - "description": "The email addresses of the contact", - "type": "array", - "items": { - "$ref": "#/components/schemas/Email" - } + "number_of_employees": { + "type": "number", + "description": "The number of employees of the company" }, - "phone_numbers": { - "description": "The phone numbers of the contact", + "user_id": { + "type": "string", + "description": "The uuid of the user who owns the company" + }, + "email_addresses": { + "description": "The email addresses of the company", "type": "array", "items": { - "$ref": "#/components/schemas/Phone" + "$ref": "#/components/schemas/Email" } }, "addresses": { - "description": "The addresses of the contact", + "description": "The addresses of the company", "type": "array", "items": { "$ref": "#/components/schemas/Address" } }, - "user_id": { - "type": "string", - "description": "The uuid of the user who owns the contact" + "phone_numbers": { + "description": "The phone numbers of the company", + "type": "array", + "items": { + "$ref": "#/components/schemas/Phone" + } }, "field_mappings": { "type": "object", @@ -5076,37 +5131,28 @@ } }, "required": [ - "first_name", - "last_name", + "name", "field_mappings" ] }, - "UnifiedDealOutput": { + "UnifiedContactOutput": { "type": "object", "properties": { "name": { "type": "string", - "description": "The name of the deal" - }, - "description": { - "type": "string", - "description": "The description of the deal" - }, - "amount": { - "type": "number", - "description": "The amount of the deal" + "description": "The name of the contact" }, - "user_id": { + "email_address": { "type": "string", - "description": "The uuid of the user who is on the deal" + "description": "The email address of the contact" }, - "stage_id": { + "phone_number": { "type": "string", - "description": "The uuid of the stage of the deal" + "description": "The phone number of the contact" }, - "company_id": { + "details": { "type": "string", - "description": "The uuid of the company tied to the deal" + "description": "The details of the contact" }, "field_mappings": { "type": "object", @@ -5114,11 +5160,11 @@ }, "id": { "type": "string", - "description": "The uuid of the deal" + "description": "The uuid of the contact" }, "remote_id": { "type": "string", - "description": "The id of the deal in the context of the Crm 3rd Party" + "description": "The id of the contact in the context of the 3rd Party" }, "remote_data": { "type": "object", @@ -5127,119 +5173,46 @@ }, "required": [ "name", - "description", - "amount", + "email_address", "field_mappings", "remote_data" ] }, - "UnifiedDealInput": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "The name of the deal" - }, - "description": { - "type": "string", - "description": "The description of the deal" - }, - "amount": { - "type": "number", - "description": "The amount of the deal" - }, - "user_id": { - "type": "string", - "description": "The uuid of the user who is on the deal" - }, - "stage_id": { - "type": "string", - "description": "The uuid of the stage of the deal" - }, - "company_id": { - "type": "string", - "description": "The uuid of the company tied to the deal" - }, - "field_mappings": { - "type": "object", - "properties": {} - } - }, - "required": [ - "name", - "description", - "amount", - "field_mappings" - ] - }, - "UnifiedNoteOutput": { + "UnifiedContactInput": { "type": "object", "properties": { - "content": { - "type": "string", - "description": "The content of the note" - }, - "user_id": { - "type": "string", - "description": "The uuid of the user tied the note" - }, - "company_id": { - "type": "string", - "description": "The uuid of the company tied to the note" - }, - "contact_id": { + "first_name": { "type": "string", - "description": "The uuid fo the contact tied to the note" + "description": "The first name of the contact" }, - "deal_id": { + "last_name": { "type": "string", - "description": "The uuid of the deal tied to the note" - }, - "field_mappings": { - "type": "object", - "properties": {} + "description": "The last name of the contact" }, - "id": { - "type": "string", - "description": "The uuid of the note" + "email_addresses": { + "description": "The email addresses of the contact", + "type": "array", + "items": { + "$ref": "#/components/schemas/Email" + } }, - "remote_id": { - "type": "string", - "description": "The id of the note in the context of the Crm 3rd Party" + "phone_numbers": { + "description": "The phone numbers of the contact", + "type": "array", + "items": { + "$ref": "#/components/schemas/Phone" + } }, - "remote_data": { - "type": "object", - "properties": {} - } - }, - "required": [ - "content", - "field_mappings", - "remote_data" - ] - }, - "UnifiedNoteInput": { - "type": "object", - "properties": { - "content": { - "type": "string", - "description": "The content of the note" + "addresses": { + "description": "The addresses of the contact", + "type": "array", + "items": { + "$ref": "#/components/schemas/Address" + } }, "user_id": { "type": "string", - "description": "The uuid of the user tied the note" - }, - "company_id": { - "type": "string", - "description": "The uuid of the company tied to the note" - }, - "contact_id": { - "type": "string", - "description": "The uuid fo the contact tied to the note" - }, - "deal_id": { - "type": "string", - "description": "The uuid of the deal tied to the note" + "description": "The uuid of the user who owns the contact" }, "field_mappings": { "type": "object", @@ -5247,49 +5220,37 @@ } }, "required": [ - "content", + "first_name", + "last_name", "field_mappings" ] }, - "UnifiedCompanyOutput": { + "UnifiedDealOutput": { "type": "object", "properties": { "name": { "type": "string", - "description": "The name of the company" + "description": "The name of the deal" }, - "industry": { + "description": { "type": "string", - "description": "The industry of the company" + "description": "The description of the deal" }, - "number_of_employees": { + "amount": { "type": "number", - "description": "The number of employees of the company" + "description": "The amount of the deal" }, "user_id": { "type": "string", - "description": "The uuid of the user who owns the company" - }, - "email_addresses": { - "description": "The email addresses of the company", - "type": "array", - "items": { - "$ref": "#/components/schemas/Email" - } + "description": "The uuid of the user who is on the deal" }, - "addresses": { - "description": "The addresses of the company", - "type": "array", - "items": { - "$ref": "#/components/schemas/Address" - } + "stage_id": { + "type": "string", + "description": "The uuid of the stage of the deal" }, - "phone_numbers": { - "description": "The phone numbers of the company", - "type": "array", - "items": { - "$ref": "#/components/schemas/Phone" - } + "company_id": { + "type": "string", + "description": "The uuid of the company tied to the deal" }, "field_mappings": { "type": "object", @@ -5297,11 +5258,11 @@ }, "id": { "type": "string", - "description": "The uuid of the company" + "description": "The uuid of the deal" }, "remote_id": { "type": "string", - "description": "The id of the company in the context of the Crm 3rd Party" + "description": "The id of the deal in the context of the Crm 3rd Party" }, "remote_data": { "type": "object", @@ -5310,49 +5271,38 @@ }, "required": [ "name", + "description", + "amount", "field_mappings", "remote_data" ] }, - "UnifiedCompanyInput": { + "UnifiedDealInput": { "type": "object", "properties": { "name": { "type": "string", - "description": "The name of the company" + "description": "The name of the deal" }, - "industry": { + "description": { "type": "string", - "description": "The industry of the company" + "description": "The description of the deal" }, - "number_of_employees": { + "amount": { "type": "number", - "description": "The number of employees of the company" + "description": "The amount of the deal" }, "user_id": { "type": "string", - "description": "The uuid of the user who owns the company" - }, - "email_addresses": { - "description": "The email addresses of the company", - "type": "array", - "items": { - "$ref": "#/components/schemas/Email" - } + "description": "The uuid of the user who is on the deal" }, - "addresses": { - "description": "The addresses of the company", - "type": "array", - "items": { - "$ref": "#/components/schemas/Address" - } + "stage_id": { + "type": "string", + "description": "The uuid of the stage of the deal" }, - "phone_numbers": { - "description": "The phone numbers of the company", - "type": "array", - "items": { - "$ref": "#/components/schemas/Phone" - } + "company_id": { + "type": "string", + "description": "The uuid of the company tied to the deal" }, "field_mappings": { "type": "object", @@ -5361,6 +5311,8 @@ }, "required": [ "name", + "description", + "amount", "field_mappings" ] }, @@ -5373,7 +5325,7 @@ }, "direction": { "type": "string", - "description": "The direction of the engagement" + "description": "The direction of the engagement. Authorized values are INBOUND or OUTBOUND" }, "subject": { "type": "string", @@ -5440,7 +5392,7 @@ }, "direction": { "type": "string", - "description": "The direction of the engagement" + "description": "The direction of the engagement. Authorized values are INBOUND or OUTBOUND" }, "subject": { "type": "string", @@ -5485,6 +5437,85 @@ "field_mappings" ] }, + "UnifiedNoteOutput": { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "The content of the note" + }, + "user_id": { + "type": "string", + "description": "The uuid of the user tied the note" + }, + "company_id": { + "type": "string", + "description": "The uuid of the company tied to the note" + }, + "contact_id": { + "type": "string", + "description": "The uuid fo the contact tied to the note" + }, + "deal_id": { + "type": "string", + "description": "The uuid of the deal tied to the note" + }, + "field_mappings": { + "type": "object", + "properties": {} + }, + "id": { + "type": "string", + "description": "The uuid of the note" + }, + "remote_id": { + "type": "string", + "description": "The id of the note in the context of the Crm 3rd Party" + }, + "remote_data": { + "type": "object", + "properties": {} + } + }, + "required": [ + "content", + "field_mappings", + "remote_data" + ] + }, + "UnifiedNoteInput": { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "The content of the note" + }, + "user_id": { + "type": "string", + "description": "The uuid of the user tied the note" + }, + "company_id": { + "type": "string", + "description": "The uuid of the company tied to the note" + }, + "contact_id": { + "type": "string", + "description": "The uuid fo the contact tied to the note" + }, + "deal_id": { + "type": "string", + "description": "The uuid of the deal tied to the note" + }, + "field_mappings": { + "type": "object", + "properties": {} + } + }, + "required": [ + "content", + "field_mappings" + ] + }, "UnifiedStageOutput": { "type": "object", "properties": { @@ -5528,7 +5559,7 @@ }, "status": { "type": "string", - "description": "The status of the task. Authorized values are \"Completed\" and \"Not Completed\" " + "description": "The status of the task. Authorized values are PENDING, COMPLETED." }, "due_date": { "format": "date-time", @@ -5590,7 +5621,7 @@ }, "status": { "type": "string", - "description": "The status of the task. Authorized values are \"Completed\" and \"Not Completed\" " + "description": "The status of the task. Authorized values are PENDING, COMPLETED." }, "due_date": { "format": "date-time", @@ -5645,11 +5676,8 @@ } }, "account_id": { - "description": "The account or organization the user is part of", - "type": "array", - "items": { - "type": "string" - } + "type": "string", + "description": "The account or organization the user is part of" }, "field_mappings": { "type": "object", @@ -5675,134 +5703,31 @@ "remote_data" ] }, - "UnifiedCommentInput": { - "type": "object", - "properties": { - "body": { - "type": "string", - "description": "The body of the comment" - }, - "html_body": { - "type": "string", - "description": "The html body of the comment" - }, - "is_private": { - "type": "boolean", - "description": "The public status of the comment" - }, - "creator_type": { - "type": "string", - "description": "The creator type of the comment (either user or contact)" - }, - "ticket_id": { - "type": "string", - "description": "The uuid of the ticket the comment is tied to" - }, - "contact_id": { - "type": "string", - "description": "The uuid of the contact which the comment belongs to (if no user_id specified)" - }, - "user_id": { - "type": "string", - "description": "The uuid of the user which the comment belongs to (if no contact_id specified)" - }, - "attachments": { - "description": "The attachements uuids tied to the comment", - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "body" - ] - }, - "UnifiedTicketOutput": { + "UnifiedAccountOutput": { "type": "object", "properties": { "name": { "type": "string", - "description": "The name of the ticket" - }, - "status": { - "type": "string", - "description": "The status of the ticket" - }, - "description": { - "description": "The description of the ticket", - "type": "array", - "items": { - "type": "string" - } - }, - "due_date": { - "format": "date-time", - "type": "string", - "description": "The date the ticket is due" - }, - "type": { - "type": "string", - "description": "The type of the ticket" - }, - "parent_ticket": { - "type": "string", - "description": "The uuid of the parent ticket" - }, - "project_id": { - "type": "string", - "description": "The uuid of the project the ticket belongs to" - }, - "tags": { - "description": "The tags names of the ticket", - "type": "array", - "items": { - "type": "string" - } - }, - "completed_at": { - "format": "date-time", - "type": "string", - "description": "The date the ticket has been completed" - }, - "priority": { - "type": "string", - "description": "The priority of the ticket" + "description": "The name of the account" }, - "assigned_to": { - "description": "The users uuids the ticket is assigned to", + "domains": { + "description": "The domains of the account", "type": "array", "items": { "type": "string" } }, - "comment": { - "description": "The comment of the ticket", - "allOf": [ - { - "$ref": "#/components/schemas/UnifiedCommentInput" - } - ] - }, - "account_id": { - "type": "string", - "description": "The uuid of the account which the ticket belongs to" - }, - "contact_id": { - "type": "string", - "description": "The uuid of the contact which the ticket belongs to" - }, "field_mappings": { "type": "object", "properties": {} }, "id": { "type": "string", - "description": "The uuid of the ticket" + "description": "The uuid of the account" }, "remote_id": { "type": "string", - "description": "The id of the ticket in the context of the 3rd Party" + "description": "The id of the account in the context of the 3rd Party" }, "remote_data": { "type": "object", @@ -5811,94 +5736,41 @@ }, "required": [ "name", - "description", "field_mappings", "remote_data" ] }, - "UnifiedTicketInput": { + "UnifiedCollectionOutput": { "type": "object", "properties": { "name": { - "type": "string", - "description": "The name of the ticket" - }, - "status": { - "type": "string", - "description": "The status of the ticket" - }, - "description": { - "description": "The description of the ticket", - "type": "array", - "items": { - "type": "string" - } - }, - "due_date": { - "format": "date-time", - "type": "string", - "description": "The date the ticket is due" - }, - "type": { - "type": "string", - "description": "The type of the ticket" - }, - "parent_ticket": { - "type": "string", - "description": "The uuid of the parent ticket" - }, - "project_id": { - "type": "string", - "description": "The uuid of the project the ticket belongs to" - }, - "tags": { - "description": "The tags names of the ticket", - "type": "array", - "items": { - "type": "string" - } - }, - "completed_at": { - "format": "date-time", - "type": "string", - "description": "The date the ticket has been completed" - }, - "priority": { - "type": "string", - "description": "The priority of the ticket" - }, - "assigned_to": { - "description": "The users uuids the ticket is assigned to", - "type": "array", - "items": { - "type": "string" - } + "type": "string", + "description": "The name of the collection" }, - "comment": { - "description": "The comment of the ticket", - "allOf": [ - { - "$ref": "#/components/schemas/UnifiedCommentInput" - } - ] + "description": { + "type": "string", + "description": "The description of the collection" }, - "account_id": { + "collection_type": { "type": "string", - "description": "The uuid of the account which the ticket belongs to" + "description": "The type of the collection. Authorized values are either PROJECT or LIST " }, - "contact_id": { + "id": { "type": "string", - "description": "The uuid of the contact which the ticket belongs to" + "description": "The uuid of the collection" }, - "field_mappings": { + "remote_id": { + "type": "string", + "description": "The id of the collection in the context of the 3rd Party" + }, + "remote_data": { "type": "object", "properties": {} } }, "required": [ "name", - "description", - "field_mappings" + "remote_data" ] }, "UnifiedAttachmentOutput": { @@ -5958,7 +5830,7 @@ }, "creator_type": { "type": "string", - "description": "The creator type of the comment (either user or contact)" + "description": "The creator type of the comment. Authorized values are either USER or CONTACT" }, "ticket_id": { "type": "string", @@ -5997,46 +5869,55 @@ "remote_data" ] }, - "UnifiedAttachmentInput": { + "UnifiedCommentInput": { "type": "object", "properties": { - "file_name": { + "body": { "type": "string", - "description": "The file name of the attachment" + "description": "The body of the comment" }, - "file_url": { + "html_body": { "type": "string", - "description": "The file url of the attachment" + "description": "The html body of the comment" }, - "uploader": { + "is_private": { + "type": "boolean", + "description": "The public status of the comment" + }, + "creator_type": { "type": "string", - "description": "The uploader's uuid of the attachment" + "description": "The creator type of the comment. Authorized values are either USER or CONTACT" }, - "field_mappings": { - "type": "object", - "properties": {} + "ticket_id": { + "type": "string", + "description": "The uuid of the ticket the comment is tied to" + }, + "contact_id": { + "type": "string", + "description": "The uuid of the contact which the comment belongs to (if no user_id specified)" + }, + "user_id": { + "type": "string", + "description": "The uuid of the user which the comment belongs to (if no contact_id specified)" + }, + "attachments": { + "description": "The attachements uuids tied to the comment", + "type": "array", + "items": { + "type": "string" + } } }, "required": [ - "file_name", - "file_url", - "uploader", - "field_mappings" + "body" ] }, - "UnifiedAccountOutput": { + "UnifiedTagOutput": { "type": "object", "properties": { "name": { "type": "string", - "description": "The name of the account" - }, - "domains": { - "description": "The domains of the account", - "type": "array", - "items": { - "type": "string" - } + "description": "The name of the tag" }, "field_mappings": { "type": "object", @@ -6044,11 +5925,11 @@ }, "id": { "type": "string", - "description": "The uuid of the account" + "description": "The uuid of the tag" }, "remote_id": { "type": "string", - "description": "The id of the account in the context of the 3rd Party" + "description": "The id of the tag in the context of the 3rd Party" }, "remote_data": { "type": "object", @@ -6061,12 +5942,16 @@ "remote_data" ] }, - "UnifiedTagOutput": { + "UnifiedTeamOutput": { "type": "object", "properties": { "name": { "type": "string", - "description": "The name of the tag" + "description": "The name of the team" + }, + "description": { + "type": "string", + "description": "The description of the team" }, "field_mappings": { "type": "object", @@ -6074,11 +5959,11 @@ }, "id": { "type": "string", - "description": "The uuid of the tag" + "description": "The uuid of the team" }, "remote_id": { "type": "string", - "description": "The id of the tag in the context of the 3rd Party" + "description": "The id of the team in the context of the 3rd Party" }, "remote_data": { "type": "object", @@ -6091,16 +5976,76 @@ "remote_data" ] }, - "UnifiedTeamOutput": { + "UnifiedTicketOutput": { "type": "object", "properties": { "name": { "type": "string", - "description": "The name of the team" + "description": "The name of the ticket" + }, + "status": { + "type": "string", + "description": "The status of the ticket. Authorized values are OPEN or CLOSED." }, "description": { "type": "string", - "description": "The description of the team" + "description": "The description of the ticket" + }, + "due_date": { + "format": "date-time", + "type": "string", + "description": "The date the ticket is due" + }, + "type": { + "type": "string", + "description": "The type of the ticket. Authorized values are PROBLEM, QUESTION, or TASK" + }, + "parent_ticket": { + "type": "string", + "description": "The uuid of the parent ticket" + }, + "project_id": { + "type": "string", + "description": "The uuid of the collection (project) the ticket belongs to" + }, + "tags": { + "description": "The tags names of the ticket", + "type": "array", + "items": { + "type": "string" + } + }, + "completed_at": { + "format": "date-time", + "type": "string", + "description": "The date the ticket has been completed" + }, + "priority": { + "type": "string", + "description": "The priority of the ticket. Authorized values are HIGH, MEDIUM or LOW." + }, + "assigned_to": { + "description": "The users uuids the ticket is assigned to", + "type": "array", + "items": { + "type": "string" + } + }, + "comment": { + "description": "The comment of the ticket", + "allOf": [ + { + "$ref": "#/components/schemas/UnifiedCommentInput" + } + ] + }, + "account_id": { + "type": "string", + "description": "The uuid of the account which the ticket belongs to" + }, + "contact_id": { + "type": "string", + "description": "The uuid of the contact which the ticket belongs to" }, "field_mappings": { "type": "object", @@ -6108,11 +6053,11 @@ }, "id": { "type": "string", - "description": "The uuid of the team" + "description": "The uuid of the ticket" }, "remote_id": { "type": "string", - "description": "The id of the team in the context of the 3rd Party" + "description": "The id of the ticket in the context of the 3rd Party" }, "remote_data": { "type": "object", @@ -6121,41 +6066,118 @@ }, "required": [ "name", + "description", "field_mappings", "remote_data" ] }, - "UnifiedCollectionOutput": { + "UnifiedTicketInput": { "type": "object", "properties": { "name": { "type": "string", - "description": "The name of the collection" + "description": "The name of the ticket" + }, + "status": { + "type": "string", + "description": "The status of the ticket. Authorized values are OPEN or CLOSED." }, "description": { "type": "string", - "description": "The description of the collection" + "description": "The description of the ticket" }, - "collection_type": { + "due_date": { + "format": "date-time", + "type": "string", + "description": "The date the ticket is due" + }, + "type": { "type": "string", - "description": "The type of the collection, either PROJECT or LIST " + "description": "The type of the ticket. Authorized values are PROBLEM, QUESTION, or TASK" }, - "id": { + "parent_ticket": { "type": "string", - "description": "The uuid of the collection" + "description": "The uuid of the parent ticket" }, - "remote_id": { + "project_id": { "type": "string", - "description": "The id of the collection in the context of the 3rd Party" + "description": "The uuid of the collection (project) the ticket belongs to" }, - "remote_data": { + "tags": { + "description": "The tags names of the ticket", + "type": "array", + "items": { + "type": "string" + } + }, + "completed_at": { + "format": "date-time", + "type": "string", + "description": "The date the ticket has been completed" + }, + "priority": { + "type": "string", + "description": "The priority of the ticket. Authorized values are HIGH, MEDIUM or LOW." + }, + "assigned_to": { + "description": "The users uuids the ticket is assigned to", + "type": "array", + "items": { + "type": "string" + } + }, + "comment": { + "description": "The comment of the ticket", + "allOf": [ + { + "$ref": "#/components/schemas/UnifiedCommentInput" + } + ] + }, + "account_id": { + "type": "string", + "description": "The uuid of the account which the ticket belongs to" + }, + "contact_id": { + "type": "string", + "description": "The uuid of the contact which the ticket belongs to" + }, + "field_mappings": { "type": "object", "properties": {} } }, "required": [ "name", - "remote_data" + "description", + "field_mappings" + ] + }, + "UnifiedAttachmentInput": { + "type": "object", + "properties": { + "file_name": { + "type": "string", + "description": "The file name of the attachment" + }, + "file_url": { + "type": "string", + "description": "The file url of the attachment" + }, + "uploader": { + "type": "string", + "description": "The uploader's uuid of the attachment" + }, + "field_mappings": { + "type": "object", + "properties": {} + } + }, + "required": [ + "file_name", + "file_url", + "uploader", + "field_mappings" ] } } diff --git a/packages/shared/src/enum.ts b/packages/shared/src/enum.ts index 656eb63b4..3d86d97f2 100644 --- a/packages/shared/src/enum.ts +++ b/packages/shared/src/enum.ts @@ -6,7 +6,6 @@ export enum ProviderVertical { Ticketing = 'ticketing', MarketingAutomation = 'marketingautomation', FileStorage = 'filestorage', - Unknown = 'unknown', } export enum CrmProviders { diff --git a/packages/shared/src/providers.ts b/packages/shared/src/providers.ts index abc0a38db..382a4e17b 100644 --- a/packages/shared/src/providers.ts +++ b/packages/shared/src/providers.ts @@ -11,7 +11,7 @@ export const TICKETING_PROVIDERS = ['zendesk', 'front', 'github', 'jira', 'gorgi export const MARKETINGAUTOMATION_PROVIDERS = ['']; export const FILESTORAGE_PROVIDERS = ['']; -export function getProviderVertical(providerName: string): ProviderVertical { +/*export function getProviderVertical(providerName: string): ProviderVertical { if (CRM_PROVIDERS.includes(providerName)) { return ProviderVertical.CRM; } @@ -33,8 +33,8 @@ export function getProviderVertical(providerName: string): ProviderVertical { if (FILESTORAGE_PROVIDERS.includes(providerName)) { return ProviderVertical.FileStorage; } - return ProviderVertical.Unknown; -} + return undefined; +}*/ function mergeAllProviders(...arrays: string[][]): { vertical: string, value: string }[] { const result: { vertical: string, value: string }[] = []; diff --git a/packages/shared/src/utils.ts b/packages/shared/src/utils.ts index 65a3d83f7..de2af40e8 100644 --- a/packages/shared/src/utils.ts +++ b/packages/shared/src/utils.ts @@ -323,9 +323,8 @@ export const providersConfig: ProvidersConfig = { }, 'ticketing': { 'front': { - scopes: '', urls: { - docsUrl: '', + docsUrl: 'https://dev.frontapp.com/docs/welcome', authBaseUrl: 'https://app.frontapp.com/oauth/authorize', apiUrl: 'https://api2.frontapp.com', }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 013adf21a..6c71560aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -376,7 +376,7 @@ importers: specifier: ^0.5.1 version: 0.5.1 class-validator: - specifier: ^0.14.0 + specifier: ^0.14.1 version: 0.14.1 cookie-parser: specifier: ^1.4.6 @@ -399,6 +399,9 @@ importers: nestjs-pino: specifier: ^3.5.0 version: 3.5.0(@nestjs/common@10.3.7)(pino-http@8.6.1) + openai: + specifier: ^4.38.5 + version: 4.38.5 passport: specifier: ^0.6.0 version: 0.6.0 @@ -4630,10 +4633,23 @@ packages: resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} dev: false + /@types/node-fetch@2.6.11: + resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==} + dependencies: + '@types/node': 20.12.7 + form-data: 4.0.0 + dev: false + /@types/node@12.20.55: resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} dev: false + /@types/node@18.19.31: + resolution: {integrity: sha512-ArgCD39YpyyrtFKIqMDvjz79jto5fcI/SVUs2HwB+f0dAzq68yqOdyaSivLiLugSziTpNXLQrVb7RZFmdZzbhA==} + dependencies: + undici-types: 5.26.5 + dev: false + /@types/node@20.12.7: resolution: {integrity: sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==} dependencies: @@ -5206,6 +5222,13 @@ packages: - supports-color dev: false + /agentkeepalive@4.5.0: + resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==} + engines: {node: '>= 8.0.0'} + dependencies: + humanize-ms: 1.2.1 + dev: false + /ajv-formats@2.1.1(ajv@8.12.0): resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -7776,6 +7799,10 @@ packages: webpack: 5.90.1 dev: true + /form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + dev: false + /form-data-encoder@2.1.4: resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} engines: {node: '>= 14.17'} @@ -7789,6 +7816,14 @@ packages: combined-stream: 1.0.8 mime-types: 2.1.35 + /formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + dev: false + /formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -8304,6 +8339,12 @@ packages: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} + /humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + dependencies: + ms: 2.1.3 + dev: false + /husky@8.0.3: resolution: {integrity: sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==} engines: {node: '>=14'} @@ -10275,6 +10316,22 @@ packages: dependencies: mimic-fn: 4.0.0 + /openai@4.38.5: + resolution: {integrity: sha512-Ym5GJL98ZhLJJ7enBx53jjG3vwN/fsB+Ozh46nnRZZS9W1NiYqbwkJ+sXd3dkCIiWIgcyyOPL2Zr8SQAzbpj3g==} + hasBin: true + dependencies: + '@types/node': 18.19.31 + '@types/node-fetch': 2.6.11 + abort-controller: 3.0.0 + agentkeepalive: 4.5.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + web-streams-polyfill: 3.3.3 + transitivePeerDependencies: + - encoding + dev: false + /optional@0.1.4: resolution: {integrity: sha512-gtvrrCfkE08wKcgXaVwQVgwEQ8vel2dc5DDBn9RLQZ3YtmtkBss6A2HY6BnJH4N/4Ku97Ri/SF8sNWE2225WJw==} dev: false @@ -13528,6 +13585,11 @@ packages: engines: {node: '>= 8'} dev: false + /web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + dev: false + /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}